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 fetch command to get manifest of an artifact #449

Merged
merged 65 commits into from
Aug 15, 2022
Merged
Show file tree
Hide file tree
Changes from 64 commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
04bfc87
Add get-manifest command with pretty option
qweeah Jul 18, 2022
c1da0e9
Add error
qweeah Jul 18, 2022
78b1e66
Rename to fetch
qweeah Jul 19, 2022
269bb01
Add media type support and refactor
qweeah Jul 19, 2022
e87f4af
Add platform and media type
qweeah Jul 20, 2022
b902a7f
Moving multi-arch options into a dedicated type.
qweeah Jul 20, 2022
b79f8e4
Enhance platform
qweeah Jul 20, 2022
994f495
Updage long description
qweeah Jul 20, 2022
8c263ba
Refactor platform
qweeah Jul 20, 2022
ad97af8
Add get-manifest command with pretty option
qweeah Jul 18, 2022
f5877e5
Add error
qweeah Jul 18, 2022
46f032d
Rename to fetch
qweeah Jul 19, 2022
4bac348
Add media type support and refactor
qweeah Jul 19, 2022
3bed958
Add platform and media type
qweeah Jul 20, 2022
d0640c7
Moving multi-arch options into a dedicated type.
qweeah Jul 20, 2022
25b5e51
Enhance platform
qweeah Jul 20, 2022
9a5dbe7
Updage long description
qweeah Jul 20, 2022
19f4451
Refactor platform
qweeah Jul 20, 2022
2d11906
Combine platform parsing
qweeah Jul 22, 2022
7908bf6
Merge branch 'get-manifest' of https://github.com/qweeah/oras into ge…
qweeah Jul 22, 2022
6da78a2
Add tests for platform.parse
qweeah Jul 23, 2022
5849406
Add os version example
qweeah Jul 24, 2022
4c7b117
Add manifest and fetch subcommand
qweeah Jul 26, 2022
bc2428b
Merge remote-tracking branch 'origin_src' into get-manifest
qweeah Jul 27, 2022
cd9ed13
Restructured
qweeah Jul 27, 2022
864bb08
Merge remote-tracking branch 'origin_src/main' into get-manifest
qweeah Jul 27, 2022
275b801
Use reference fetcher
qweeah Jul 27, 2022
d905883
Use FetchReference and promoted content.FetchAll
qweeah Jul 27, 2022
3369ac3
Add mocked unit test
qweeah Jul 27, 2022
0af3746
Refactor and add unit tests
qweeah Jul 27, 2022
1553423
More coverage
qweeah Jul 27, 2022
bc5ce3e
Resolve comment
qweeah Jul 28, 2022
b3ae291
Add fetch descriptor
qweeah Jul 28, 2022
7c6d00b
Add fetching descriptor
qweeah Jul 28, 2022
8fc62b5
Add test for fetch descriptor
qweeah Jul 28, 2022
7b0989c
Use unmerged branch
qweeah Aug 2, 2022
f59912e
Add coverage
qweeah Aug 2, 2022
7145418
Add mock package
qweeah Aug 2, 2022
516526e
Code clean
qweeah Aug 3, 2022
d58db42
Code clean
qweeah Aug 3, 2022
00ed116
Merge remote-tracking branch 'origin_src/main' into get-manifest
qweeah Aug 10, 2022
cd910f3
Use strings.Cut
qweeah Aug 10, 2022
bc503e9
Code clean
qweeah Aug 10, 2022
89b2747
Merge remote-tracking branch 'origin_src/main' into get-manifest
qweeah Aug 10, 2022
c9157c8
Merge remote-tracking branch 'origin_src/main' into get-manifest
qweeah Aug 15, 2022
96b88d6
Add caching
qweeah Aug 15, 2022
983d7b8
Code clean
qweeah Aug 15, 2022
3eb653f
Merge remote-tracking branch 'origin_src/main' into get-manifest
qweeah Aug 15, 2022
037561a
Remod
qweeah Aug 15, 2022
81bf60f
Remod to rc 2
qweeah Aug 15, 2022
335ee91
Code clean
qweeah Aug 15, 2022
b219e00
Refactor: move cas to a dedicated package
qweeah Aug 15, 2022
1722b16
Merge branch 'main' into get-manifest
qweeah Aug 15, 2022
39a71da
Code clean
qweeah Aug 15, 2022
b0a5fb2
Add code coverage
qweeah Aug 15, 2022
2168f22
Add descriptor example
qweeah Aug 15, 2022
b2223d0
Add platform example
qweeah Aug 15, 2022
7957e58
Code clean
qweeah Aug 15, 2022
f05e7fa
Code clean
qweeah Aug 15, 2022
2ac5fce
Code clean
qweeah Aug 15, 2022
349160f
Change media type to array
qweeah Aug 15, 2022
3a09ca7
Refactor fetching
qweeah Aug 15, 2022
2574a80
Add default arch if not provided
qweeah Aug 15, 2022
e3f2c90
Code clean
qweeah Aug 15, 2022
851356f
Fix unit test
qweeah Aug 15, 2022
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 cmd/oras/discover.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ import (
"strings"

"oras.land/oras-go/v2/registry/remote"
"oras.land/oras/cmd/oras/internal/errors"
"oras.land/oras/cmd/oras/internal/option"

"github.com/need-being/go-tree"
digest "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
artifactspec "github.com/oras-project/artifacts-spec/specs-go/v1"
"github.com/spf13/cobra"
"oras.land/oras/cmd/oras/internal/errors"
)

type discoverOptions struct {
Expand Down
2 changes: 1 addition & 1 deletion cmd/oras/internal/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import (
"oras.land/oras-go/v2/registry"
)

// NewErrInvalidReference creates a new error based on the reference string.
// NewErrInvalidReference creates a new error based on the reference string.
func NewErrInvalidReference(ref registry.Reference) error {
return fmt.Errorf("%s: invalid image reference, expecting <name:tag|name@digest>", ref)
}
68 changes: 68 additions & 0 deletions cmd/oras/internal/option/platform.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
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 option

import (
"fmt"
"runtime"
"strings"

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

// Platform option struct.
type Platform struct {
Platform string
}

// ApplyFlags applies flags to a command flag set.
func (opts *Platform) ApplyFlags(fs *pflag.FlagSet) {
fs.StringVarP(&opts.Platform, "platform", "", "", "fetch the manifest of a specific platform if target is multi-platform capable")
}

// parse parses the input platform flag to an oci platform type.
func (opts *Platform) Parse() (*ocispec.Platform, error) {
if opts.Platform == "" {
return nil, nil
}

// OS[/Arch[/Variant]][:OSVersion]
// If Arch is not provided, will use GOARCH instead
var platformStr string
var p ocispec.Platform
platformStr, p.OSVersion, _ = strings.Cut(opts.Platform, ":")
parts := strings.Split(platformStr, "/")
switch len(parts) {
case 3:
p.Variant = parts[2]
fallthrough
case 2:
p.Architecture = parts[1]
case 1:
p.Architecture = runtime.GOARCH
default:
return nil, fmt.Errorf("failed to parse platform %q: expected format os[/arch[/variant]]", opts.Platform)
}
p.OS = parts[0]
if p.OS == "" {
return nil, fmt.Errorf("invalid platform: OS cannot be empty")
}
if p.Architecture == "" {
return nil, fmt.Errorf("invalid platform: Architecture cannot be empty")
}
return &p, nil
}
83 changes: 83 additions & 0 deletions cmd/oras/internal/option/platform_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
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 option

import (
"reflect"
"runtime"
"testing"

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

func TestPlatform_ApplyFlags(t *testing.T) {
var test struct{ Platform }
ApplyFlags(&test, pflag.NewFlagSet("oras-test", pflag.ExitOnError))
if test.Platform.Platform != "" {
t.Fatalf("expecting platform to be empty but got: %v", test.Platform.Platform)
}
}

func TestPlatform_Parse_err(t *testing.T) {
tests := []struct {
name string
opts *Platform
}{
{name: "empty arch 1", opts: &Platform{"os/"}},
{name: "empty arch 2", opts: &Platform{"os//variant"}},
{name: "empty os", opts: &Platform{"/arch"}},
{name: "empty os with variant", opts: &Platform{"/arch/variant"}},
{name: "trailing slash", opts: &Platform{"os/arch/variant/llama"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := tt.opts.Parse()
if err == nil {
t.Errorf("Platform.Parse() error = %v, wantErr %v", err, true)
return
}
})
}
}

func TestPlatform_Parse(t *testing.T) {
tests := []struct {
name string
opts *Platform
want *ocispec.Platform
}{
{name: "empty", opts: &Platform{""}, want: nil},
{name: "default arch", opts: &Platform{"os"}, want: &ocispec.Platform{OS: "os", Architecture: runtime.GOARCH}},
{name: "os&arch", opts: &Platform{"os/aRcH"}, want: &ocispec.Platform{OS: "os", Architecture: "aRcH"}},
{name: "empty variant", opts: &Platform{"os/aRcH/"}, want: &ocispec.Platform{OS: "os", Architecture: "aRcH", Variant: ""}},
{name: "os&arch&variant", opts: &Platform{"os/aRcH/vAriAnt"}, want: &ocispec.Platform{OS: "os", Architecture: "aRcH", Variant: "vAriAnt"}},
{name: "os version", opts: &Platform{"os/aRcH/vAriAnt:osversion"}, want: &ocispec.Platform{OS: "os", Architecture: "aRcH", Variant: "vAriAnt", OSVersion: "osversion"}},
{name: "long os version", opts: &Platform{"os/aRcH"}, want: &ocispec.Platform{OS: "os", Architecture: "aRcH"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.opts.Parse()
if err != nil {
t.Errorf("Platform.Parse() error = %v", err)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Platform.Parse() = %v, want %v", got, tt.want)
}
})
}
}
2 changes: 2 additions & 0 deletions cmd/oras/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"os"

"github.com/spf13/cobra"
"oras.land/oras/cmd/oras/manifest"
)

func main() {
Expand All @@ -20,6 +21,7 @@ func main() {
discoverCmd(),
copyCmd(),
attachCmd(),
manifest.Cmd(),
)
if err := cmd.Execute(); err != nil {
os.Exit(1)
Expand Down
32 changes: 32 additions & 0 deletions cmd/oras/manifest/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
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 manifest

import (
"github.com/spf13/cobra"
)

func Cmd() *cobra.Command {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we call it Command()?

cmd := &cobra.Command{
Use: "manifest [fetch]",
Short: "[Preview] Manifest operations",
}

cmd.AddCommand(
fetchCmd(),
)
return cmd
}
116 changes: 116 additions & 0 deletions cmd/oras/manifest/fetch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
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 manifest

import (
"bytes"
"encoding/json"
"fmt"
"os"

"github.com/spf13/cobra"
"oras.land/oras/cmd/oras/internal/errors"
"oras.land/oras/cmd/oras/internal/option"
"oras.land/oras/internal/cas"
)

type fetchOptions struct {
option.Common
option.Remote
option.Platform

targetRef string
pretty bool
mediaTypes []string
fetchDescriptor bool
}

func fetchCmd() *cobra.Command {
var opts fetchOptions
cmd := &cobra.Command{
Use: "fetch [flags] <name:tag|name@digest>",
Short: "[Preview] Fetch manifest of the target artifact",
Long: `[Preview] Fetch manifest of the target artifact
** This command is in preview and under development. **

Example - Fetch raw manifest:
oras manifest fetch localhost:5000/hello:latest

Example - Fetch the descriptor of a manifest:
oras manifest fetch --descriptor localhost:5000/hello:latest

Example - Fetch manifest with specified media type:
oras manifest fetch --media-type 'application/vnd.oci.image.manifest.v1+json' localhost:5000/hello:latest

Example - Fetch manifest with certain platform:
oras manifest fetch --platform 'linux/arm/v5' localhost:5000/hello:latest

Example - Fetch manifest with prettified json result:
oras manifest fetch --pretty localhost:5000/hello:latest
`,
Args: cobra.ExactArgs(1),
PreRunE: func(cmd *cobra.Command, args []string) error {
return opts.ReadPassword()
},
Aliases: []string{"get"},
RunE: func(cmd *cobra.Command, args []string) error {
opts.targetRef = args[0]
return fetchManifest(opts)
},
}

cmd.Flags().BoolVarP(&opts.pretty, "pretty", "", false, "output prettified manifest")
cmd.Flags().BoolVarP(&opts.fetchDescriptor, "descriptor", "", false, "fetch a descriptor of the manifest")
cmd.Flags().StringSliceVarP(&opts.mediaTypes, "media-type", "", nil, "accepted media types")
option.ApplyFlags(&opts, cmd.Flags())
return cmd
}

func fetchManifest(opts fetchOptions) error {
ctx, _ := opts.SetLoggerLevel()
tagetPlatform, err := opts.Parse()
if err != nil {
return err
}
repo, err := opts.NewRepository(opts.targetRef, opts.Common)
if err != nil {
return err
}
if repo.Reference.Reference == "" {
return errors.NewErrInvalidReference(repo.Reference)
}
repo.ManifestMediaTypes = opts.mediaTypes

// Fetch and output
var content []byte
if opts.fetchDescriptor {
content, err = cas.FetchDescriptor(ctx, repo, opts.targetRef, tagetPlatform)
} else {
content, err = cas.FetchManifest(ctx, repo, opts.targetRef, tagetPlatform)
}
if err != nil {
return err
}
if opts.pretty {
buf := bytes.NewBuffer(nil)
if err = json.Indent(buf, content, "", " "); err != nil {
return fmt.Errorf("failed to prettify: %w", err)
}
content = buf.Bytes()
}
_, err = os.Stdout.Write(content)
return err
}
Loading