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
25 changes: 25 additions & 0 deletions cmd/oras/internal/descriptor/descriptor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
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 descriptor

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

func IsImageManifest(mediaType string) bool {
yuehaoliang marked this conversation as resolved.
Show resolved Hide resolved
return mediaType == docker.MediaTypeManifest || mediaType == ocispec.MediaTypeImageManifest
}
35 changes: 35 additions & 0 deletions cmd/oras/internal/descriptor/descriptor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
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 descriptor

import (
"reflect"
"testing"
)

func TestIsImageManifest(t *testing.T) {
mediaType := "application/vnd.oci.image.manifest.v1+json"
got := 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 = IsImageManifest(mediaType)
if !reflect.DeepEqual(got, false) {
t.Fatalf("IsImageManifest() got %v, want %v", got, false)
}
}
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
}
176 changes: 176 additions & 0 deletions cmd/oras/manifest/fetchConfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/*
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"
"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"
"oras.land/oras/cmd/oras/internal/descriptor"
oerrors "oras.land/oras/cmd/oras/internal/errors"
"oras.land/oras/cmd/oras/internal/option"
"oras.land/oras/internal/cache"
)

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 [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:
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 --output config.json localhost:5000/hello:latest

Example - Fetch the descriptor of the config:
oras manifest fetch-config --descriptor localhost:5000/hello:latest
yuehaoliang marked this conversation as resolved.
Show resolved Hide resolved
`,
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")
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) (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)
}

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

// output 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 != "" && opts.outputPath != "-" {
file, err := os.Create(opts.outputPath)
if err != nil {
return err
}
defer func() {
if err := file.Close(); fetchErr == nil {
fetchErr = err
}
}()

if _, err = file.Write(contentBytes); err != nil {
return err
}
yuehaoliang marked this conversation as resolved.
Show resolved Hide resolved
}

// output config
if (opts.outputPath == "" && !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 && descriptor.IsImageManifest(manifestDesc.MediaType)) {
return s, nil
}
}
return ocispec.Descriptor{}, fmt.Errorf("%s does not have a config", reference)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Since we know it is an image manifest, we can call oras.FetchBytes() and then unmarshal it to extract the config descriptor. The simplified operation not only has less network calls but also cleaner.

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.

Since we know it is an image manifest, we can call oras.FetchBytes() and then unmarshal it to extract the config descriptor. The simplified operation not only has less network calls but also cleaner.

Are we sure that manifest fetch-config is only used for image manifest? I see in oras pull --config, users can provided the media type of the config file. If the manifest is not an image manifest, the command will fetch the blob who has a matched media type.

Copy link
Contributor

@shizhMSFT shizhMSFT Sep 15, 2022

Choose a reason for hiding this comment

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

Are we sure that manifest fetch-config is only used for image manifest?

Yes, manifest fetch-config should only run against image manifests.

Copy link
Contributor

Choose a reason for hiding this comment

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

oras manifest fetch-config should return error if the manifest is not 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.

Resolved as required.

8 changes: 2 additions & 6 deletions cmd/oras/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ import (
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/content/file"
"oras.land/oras-go/v2/content/oci"
"oras.land/oras/cmd/oras/internal/descriptor"
"oras.land/oras/cmd/oras/internal/display"
"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 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 && descriptor.IsImageManifest(desc.MediaType))) && configPath != "" {
yuehaoliang marked this conversation as resolved.
Show resolved Hide resolved
// 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
}