Skip to content

Commit

Permalink
feat: add manifest fetch-config (#540)
Browse files Browse the repository at this point in the history
Signed-off-by: Haoliang Yue <[email protected]>
  • Loading branch information
yuehaoliang authored Sep 19, 2022
1 parent ba2385c commit 1075b3b
Show file tree
Hide file tree
Showing 6 changed files with 252 additions and 9 deletions.
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
}
4 changes: 3 additions & 1 deletion cmd/oras/manifest/fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,9 @@ func fetchManifest(opts fetchOptions) (fetchErr error) {
var desc ocispec.Descriptor
if opts.OutputDescriptor && opts.outputPath == "" {
// fetch manifest descriptor only
desc, err = oras.Resolve(ctx, manifests, opts.targetRef, oras.DefaultResolveOptions)
fetchOpts := oras.DefaultResolveOptions
fetchOpts.TargetPlatform = targetPlatform
desc, err = oras.Resolve(ctx, manifests, opts.targetRef, fetchOpts)
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:
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
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, 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)
}
}

0 comments on commit 1075b3b

Please sign in to comment.