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
1 change: 1 addition & 0 deletions cmd/oras/manifest/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ func Cmd() *cobra.Command {

cmd.AddCommand(
fetchCmd(),
fetchConfigCmd(),
)
return cmd
}
172 changes: 172 additions & 0 deletions cmd/oras/manifest/fetchConfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/*
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
yuehaoliang marked this conversation as resolved.
Show resolved Hide resolved

import (
"context"
"encoding/json"
"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"
"oras.land/oras/cmd/oras/internal/errors"
"oras.land/oras/cmd/oras/internal/option"
"oras.land/oras/internal/cache"
"oras.land/oras/internal/docker"
)

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

cacheRoot string
mediaType string
outputPath string
targetRef string
}

func fetchConfigCmd() *cobra.Command {
var opts fetchConfigOptions
cmd := &cobra.Command{
Use: "fetch-config 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:
oras manifest fetch-config localhost:5000/hello:latest
yuehaoliang marked this conversation as resolved.
Show resolved Hide resolved
Example - Fetch the config and save it to a local file:
oras manifest fetch-config localhost:5000/hello:latest --output config.json
Example - Fetch the descriptor of the config:
oras manifest fetch-config localhost:5000/hello:latest --descriptor
`,
Args: cobra.ExactArgs(1),
PreRunE: func(cmd *cobra.Command, args []string) error {
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")
cmd.Flags().StringVarP(&opts.mediaType, "media-type", "", "", "media type of the manifest config")
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we remove this flag as it does not have a user scenario and it confuses users? /cc @FeynmanZhou @yizha1

Copy link
Contributor Author

@yuehaoliang yuehaoliang Sep 15, 2022

Choose a reason for hiding this comment

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

Can we remove this flag as it does not have a user scenario and it confuses users? /cc @FeynmanZhou @yizha1

I was tried to support the same feature as oras pull --config, but if users only use this command for an image manifest, --media-type is not required, because we can always recognize the config for an image manifest.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed --media-type for now. I think if only image manifest type support this manifest fetch-config command, this argument is unnecessary.

option.ApplyFlags(&opts, cmd.Flags())
return cmd
}

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

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

if repo.Reference.Reference == "" {
return errors.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)
}

configDesc, err := fetchConfigDesc(ctx, src, opts.targetRef, opts.mediaType)
if err != nil {
return err
}

// outputs config's descriptor if `--descriptor` is used
if opts.OutputDescriptor {
descBytes, err := json.Marshal(configDesc)
if err != nil {
return err
}
err = opts.Output(os.Stdout, descBytes)
if err != nil {
return err
}
}

contentBytes, err := content.FetchAll(ctx, src, configDesc)
if err != nil {
return err
}

// save config into the local file if the output path is provided
if opts.outputPath != "" {
file, err := os.OpenFile(opts.outputPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.ModePerm)
if err != nil {
return err
}
defer func() {
if closeErr := file.Close(); err == nil {
err = closeErr
}
}()

err = opts.Output(file, contentBytes)
if err != nil {
return err
}
}

if !opts.OutputDescriptor && opts.outputPath == "" {
err = opts.Output(os.Stdout, contentBytes)
if err != nil {
return err
}
}

return nil
}

func fetchConfigDesc(ctx context.Context, src oras.ReadOnlyTarget, reference string, configMediaType string) (ocispec.Descriptor, error) {
// fetch manifest descriptor
manifestDesc, err := oras.Resolve(ctx, src, reference, oras.ResolveOptions{})
if err != nil {
return ocispec.Descriptor{}, err
}

// fetch config descriptor
successors, err := content.Successors(ctx, src, manifestDesc)
if err != nil {
return ocispec.Descriptor{}, err
}
for i, s := range successors {
if s.MediaType == configMediaType || (configMediaType == "" && i == 0 && isImageManifest(manifestDesc.MediaType)) {
return s, nil
}
}
return ocispec.Descriptor{}, fmt.Errorf("%s does not have a config", reference)
}

func isImageManifest(mediaType string) bool {
yuehaoliang marked this conversation as resolved.
Show resolved Hide resolved
return mediaType == docker.MediaTypeManifest || mediaType == ocispec.MediaTypeImageManifest
}