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 manifest fetch-config #540

Merged
merged 14 commits into from
Sep 19, 2022
Merged
5 changes: 3 additions & 2 deletions cmd/oras/manifest/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ func Cmd() *cobra.Command {
}

cmd.AddCommand(
pushCmd(),
fetchCmd(),
deleteCmd(),
fetchCmd(),
fetchConfigCmd(),
pushCmd(),
)
return cmd
}
164 changes: 164 additions & 0 deletions cmd/oras/manifest/fetch_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/*
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 (
"context"
"encoding/json"
"errors"
"fmt"
"os"

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-go/v2/content/oci"
oerrors "oras.land/oras/cmd/oras/internal/errors"
"oras.land/oras/cmd/oras/internal/option"
"oras.land/oras/internal/cache"
"oras.land/oras/internal/file"
)

type fetchConfigOptions struct {
option.Common
option.Descriptor
option.Pretty
option.Remote

cacheRoot string
outputPath string
targetRef string
}

func fetchConfigCmd() *cobra.Command {
var opts fetchConfigOptions
cmd := &cobra.Command{
Use: "fetch-config [flag] name<:tag|@digest>",
Short: "[Preview] Fetch the config of a manifest from a remote registry",
Long: `[Preview] Fetch the config of a manifest from a remote registry

** This command is in preview and under development. **

Example - Fetch the config:
Copy link
Contributor

Choose a reason for hiding this comment

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

There is a scenario missing:

oras manifest fetch-config --platform 'linux/arm/v5' localhost:5000/hello:latest

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Supported option.Platform.

oras manifest fetch-config localhost:5000/hello:latest

Example - Fetch the config and save it to a local file:
oras manifest fetch-config --output config.json localhost:5000/hello:latest

yuehaoliang marked this conversation as resolved.
Show resolved Hide resolved
Example - Fetch the descriptor of the config:
oras manifest fetch-config --descriptor localhost:5000/hello:latest
`,
Args: cobra.ExactArgs(1),
PreRunE: func(cmd *cobra.Command, args []string) error {
if opts.outputPath == "-" && opts.OutputDescriptor {
return errors.New("`--output -` cannot be used with `--descriptor` at the same time")
}

opts.cacheRoot = os.Getenv("ORAS_CACHE")
return opts.ReadPassword()
},
RunE: func(cmd *cobra.Command, args []string) error {
opts.targetRef = args[0]
return fetchConfig(opts)
},
}

cmd.Flags().StringVarP(&opts.outputPath, "output", "o", "", "output file path")
option.ApplyFlags(&opts, cmd.Flags())
return cmd
}

func fetchConfig(opts fetchConfigOptions) (fetchErr error) {
ctx, _ := opts.SetLoggerLevel()

repo, err := opts.NewRepository(opts.targetRef, opts.Common)
if err != nil {
return err
}

if repo.Reference.Reference == "" {
return oerrors.NewErrInvalidReference(repo.Reference)
}

var src oras.ReadOnlyTarget = repo
if opts.cacheRoot != "" {
ociStore, err := oci.New(opts.cacheRoot)
if err != nil {
return err
}
src = cache.New(repo, ociStore)
}
yuehaoliang marked this conversation as resolved.
Show resolved Hide resolved

// fetch config descriptor
configDesc, err := fetchConfigDesc(ctx, src, opts.targetRef)
if err != nil {
return err
}

if !opts.OutputDescriptor || opts.outputPath != "" {
// fetch config content
contentBytes, err := content.FetchAll(ctx, src, configDesc)
if err != nil {
return err
}

// output config content
if opts.outputPath == "" || opts.outputPath == "-" {
return opts.Output(os.Stdout, contentBytes)
}

// save config into the local file if the output path is provided
if opts.outputPath != "" && opts.outputPath != "-" {
yuehaoliang marked this conversation as resolved.
Show resolved Hide resolved
if err = os.WriteFile(opts.outputPath, contentBytes, 0666); err != nil {
return err
}
}
}

// output config's descriptor if `--descriptor` is used
if opts.OutputDescriptor {
yuehaoliang marked this conversation as resolved.
Show resolved Hide resolved
descBytes, err := json.Marshal(configDesc)
if err != nil {
return err
}
err = opts.Output(os.Stdout, descBytes)
if err != nil {
return err
}
yuehaoliang marked this conversation as resolved.
Show resolved Hide resolved
}

return nil
}

func fetchConfigDesc(ctx context.Context, src oras.ReadOnlyTarget, reference string) (ocispec.Descriptor, error) {
// fetch manifest descriptor and content
manifestDesc, manifestContent, err := oras.FetchBytes(ctx, src, reference, oras.DefaultFetchBytesOptions)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
manifestDesc, manifestContent, err := oras.FetchBytes(ctx, src, reference, oras.DefaultFetchBytesOptions)
manifestDesc, manifestContent, err := oras.FetchBytes(ctx, src, reference, fetchOpts)

if err != nil {
return ocispec.Descriptor{}, err
}

if !file.IsImageManifest(manifestDesc.MediaType) {
return ocispec.Descriptor{}, fmt.Errorf("%q is not an image manifest and does not have a config", manifestDesc.Digest)
}

// unmarshal manifest content to extract config descriptor
var manifest ocispec.Manifest
if err := json.Unmarshal(manifestContent, &manifest); err != nil {
return ocispec.Descriptor{}, err
}
return manifest.Config, nil
}
8 changes: 2 additions & 6 deletions cmd/oras/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import (
"oras.land/oras/cmd/oras/internal/errors"
"oras.land/oras/cmd/oras/internal/option"
"oras.land/oras/internal/cache"
"oras.land/oras/internal/docker"
ifile "oras.land/oras/internal/file"
)

type pullOptions struct {
Expand Down Expand Up @@ -121,7 +121,7 @@ func runPull(opts pullOptions) error {
// 2) MediaType not specified and current node is config.
// Note: For a manifest, the 0th indexed element is always a
// manifest config.
if (s.MediaType == configMediaType || (configMediaType == "" && i == 0 && isManifestMediaType(desc.MediaType))) && configPath != "" {
if (s.MediaType == configMediaType || (configMediaType == "" && i == 0 && ifile.IsImageManifest(desc.MediaType))) && configPath != "" {
// Add annotation for manifest config
if s.Annotations == nil {
s.Annotations = make(map[string]string)
Expand Down Expand Up @@ -181,7 +181,3 @@ func runPull(opts pullOptions) error {
fmt.Println("Digest:", desc.Digest)
return nil
}

func isManifestMediaType(mediaType string) bool {
return mediaType == docker.MediaTypeManifest || mediaType == ocispec.MediaTypeImageManifest
}
6 changes: 6 additions & 0 deletions internal/file/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (

digest "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras/internal/docker"
)

// PrepareManifestContent prepares the content for manifest from the file path
Expand Down Expand Up @@ -131,3 +132,8 @@ func ParseMediaType(content []byte) (string, error) {
}
return manifest.MediaType, nil
}

// IsImageManifest checks whether a manifest is an image manifest.
func IsImageManifest(mediaType string) bool {
return mediaType == docker.MediaTypeManifest || mediaType == ocispec.MediaTypeImageManifest
}
yuehaoliang marked this conversation as resolved.
Show resolved Hide resolved
14 changes: 14 additions & 0 deletions internal/file/file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -306,3 +306,17 @@ func TestFile_ParseMediaType_invalidContent_missingMediaType(t *testing.T) {
t.Fatalf("ParseMediaType() error = %v, wantErr %v", err, expected)
}
}

func TestIsImageManifest(t *testing.T) {
mediaType := "application/vnd.oci.image.manifest.v1+json"
got := file.IsImageManifest(mediaType)
if !reflect.DeepEqual(got, true) {
t.Fatalf("IsImageManifest() got %v, want %v", got, true)
}

mediaType = "application/vnd.cncf.oras.artifact.manifest.v1+json"
got = file.IsImageManifest(mediaType)
if !reflect.DeepEqual(got, false) {
t.Fatalf("IsImageManifest() got %v, want %v", got, false)
}
}