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
}
2 changes: 2 additions & 0 deletions cmd/oras/manifest/fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ func fetchManifest(opts fetchOptions) (fetchErr error) {
var desc ocispec.Descriptor
if opts.OutputDescriptor && opts.outputPath == "" {
// fetch manifest descriptor only
fetchOpts := oras.DefaultResolveOptions
fetchOpts.TargetPlatform = targetPlatform
desc, err = oras.Resolve(ctx, manifests, opts.targetRef, oras.DefaultResolveOptions)
if err != nil {
return err
Expand Down
169 changes: 169 additions & 0 deletions cmd/oras/manifest/fetch_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/*
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"
oerrors "oras.land/oras/cmd/oras/internal/errors"
"oras.land/oras/cmd/oras/internal/option"
"oras.land/oras/internal/descriptor"
)

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

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 of certain platform:
oras manifest fetch-config --platform 'linux/arm/v5' localhost:5000/hello:latest

Example - Fetch and print the prettified config:
oras manifest fetch-config --pretty 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

Example - Fetch and print the prettified descriptor of the config:
oras manifest fetch-config --descriptor --pretty 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")
}

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)
}

targetPlatform, err := opts.Parse()
if err != nil {
return err
}

src, err := opts.CachedTarget(repo)
if err != nil {
return err
}

// fetch config descriptor
configDesc, err := fetchConfigDesc(ctx, src, opts.targetRef, targetPlatform)
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
}

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

// save config into the local file if the output path is provided
if err = os.WriteFile(opts.outputPath, contentBytes, 0666); err != nil {
return err
}
}

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

return nil
}

func fetchConfigDesc(ctx context.Context, src oras.ReadOnlyTarget, reference string, targetPlatform *ocispec.Platform) (ocispec.Descriptor, error) {
// fetch manifest descriptor and content
fetchOpts := oras.DefaultFetchBytesOptions
fetchOpts.TargetPlatform = targetPlatform
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 !descriptor.IsImageManifest(manifestDesc) {
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 @@ -28,7 +28,7 @@ import (
"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/docker"
"oras.land/oras/internal/descriptor"
)

type pullOptions struct {
Expand Down Expand Up @@ -113,7 +113,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))) && configPath != "" {
// Add annotation for manifest config
if s.Annotations == nil {
s.Annotations = make(map[string]string)
Expand Down Expand Up @@ -189,10 +189,6 @@ func runPull(opts pullOptions) error {
return nil
}

func isManifestMediaType(mediaType string) bool {
return mediaType == docker.MediaTypeManifest || mediaType == ocispec.MediaTypeImageManifest
}

// generateContentKey generates a unique key for each content descriptor, using
// its digest and name if applicable.
func generateContentKey(desc ocispec.Descriptor) string {
Expand Down
27 changes: 27 additions & 0 deletions internal/descriptor/descriptor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
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"
)

// IsImageManifest checks whether a manifest is an image manifest.
func IsImageManifest(desc ocispec.Descriptor) bool {
return desc.MediaType == docker.MediaTypeManifest || desc.MediaType == ocispec.MediaTypeImageManifest
}
48 changes: 48 additions & 0 deletions internal/descriptor/descriptor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
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_test

import (
"reflect"
"testing"

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

func TestIsImageManifest(t *testing.T) {
imageDesc := ocispec.Descriptor{
MediaType: "application/vnd.oci.image.manifest.v1+json",
Digest: "sha256:2e0e0fe1fb3edbcdddad941c90d2b51e25a6bcd593e82545441a216de7bfa834",
Size: 474,
}

got := descriptor.IsImageManifest(imageDesc)
if !reflect.DeepEqual(got, true) {
t.Fatalf("IsImageManifest() got %v, want %v", got, true)
}

artifactDesc := ocispec.Descriptor{
MediaType: "application/vnd.cncf.oras.artifact.manifest.v1+json",
Digest: "sha256:772fbebcda7e6937de01295bae28360afd463c2d5f1f7aca59a3ef267608bc66",
Size: 568,
}

got = descriptor.IsImageManifest(artifactDesc)
if !reflect.DeepEqual(got, false) {
t.Fatalf("IsImageManifest() got %v, want %v", got, false)
}
}