Skip to content

Commit

Permalink
rebase
Browse files Browse the repository at this point in the history
Signed-off-by: Xiaoxuan Wang <xiaoxuanwang@microsoft.com>
  • Loading branch information
Xiaoxuan Wang committed Aug 27, 2024
1 parent dbfef49 commit e939b85
Show file tree
Hide file tree
Showing 13 changed files with 783 additions and 1 deletion.
5 changes: 5 additions & 0 deletions cmd/oras/internal/display/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,11 @@ func NewManifestPushHandler(printer *output.Printer) metadata.ManifestPushHandle
return text.NewManifestPushHandler(printer)
}

// NewIndexCreateHandler returns an index create handler.
func NewIndexCreateHandler(printer *output.Printer) metadata.IndexCreateHandler {
return text.NewIndexCreateHandler(printer)
}

// NewCopyHandler returns copy handlers.
func NewCopyHandler(printer *output.Printer, fetcher fetcher.Fetcher) (status.CopyHandler, metadata.CopyHandler) {
return status.NewTextCopyHandler(printer, fetcher), text.NewCopyHandler(printer)
Expand Down
5 changes: 5 additions & 0 deletions cmd/oras/internal/display/metadata/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ type ManifestPushHandler interface {
TaggedHandler
}

// IndexCreateHandler handles metadata output for index create events.
type IndexCreateHandler interface {
TaggedHandler
}

// CopyHandler handles metadata output for cp events.
type CopyHandler interface {
TaggedHandler
Expand Down
39 changes: 39 additions & 0 deletions cmd/oras/internal/display/metadata/text/manifest_index_create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
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 text

import (
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras/cmd/oras/internal/display/metadata"
"oras.land/oras/cmd/oras/internal/output"
)

// IndexCreateHandler handles text metadata output for index create events.
type IndexCreateHandler struct {
printer *output.Printer
}

// NewIndexCreateHandler returns a new handler for index create events.
func NewIndexCreateHandler(printer *output.Printer) metadata.IndexCreateHandler {
return &IndexCreateHandler{
printer: printer,
}
}

// OnTagged implements metadata.TaggedHandler.
func (h *IndexCreateHandler) OnTagged(_ ocispec.Descriptor, tag string) error {
return h.printer.Println("Tagged", tag)
}
14 changes: 14 additions & 0 deletions cmd/oras/internal/display/status/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,20 @@ const (
copyPromptMounted = "Mounted"
)

// Prompts for index events.
const (
IndexPromptFetching = "Fetching "
IndexPromptFetched = "Fetched "
IndexPromptResolving = "Resolving"
IndexPromptResolved = "Resolved "
IndexPromptAdded = "Added "
IndexPromptMerged = "Merged "
IndexPromptRemoved = "Removed "
IndexPromptPacked = "Packed "
IndexPromptPushed = "Pushed "
IndexPromptUpdated = "Updated "
)

// DeduplicatedFilter filters out deduplicated descriptors.
func DeduplicatedFilter(committed *sync.Map) func(desc ocispec.Descriptor) bool {
return func(desc ocispec.Descriptor) bool {
Expand Down
2 changes: 2 additions & 0 deletions cmd/oras/root/manifest/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package manifest

import (
"github.com/spf13/cobra"
"oras.land/oras/cmd/oras/root/manifest/index"
)

func Cmd() *cobra.Command {
Expand All @@ -30,6 +31,7 @@ func Cmd() *cobra.Command {
fetchCmd(),
fetchConfigCmd(),
pushCmd(),
index.Cmd(),
)
return cmd
}
31 changes: 31 additions & 0 deletions cmd/oras/root/manifest/index/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
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 index

import (
"github.com/spf13/cobra"
)

func Cmd() *cobra.Command {
cmd := &cobra.Command{
Use: "index [command]",
Short: "[Experimental] Index operations",
}

cmd.AddCommand(
createCmd(),
updateCmd(),
)
return cmd
}
184 changes: 184 additions & 0 deletions cmd/oras/root/manifest/index/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/*
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 index

import (
"bytes"
"context"
"encoding/json"
"fmt"
"strings"

"github.com/opencontainers/image-spec/specs-go"
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/cmd/oras/internal/argument"
"oras.land/oras/cmd/oras/internal/command"
"oras.land/oras/cmd/oras/internal/display"
"oras.land/oras/cmd/oras/internal/display/status"
oerrors "oras.land/oras/cmd/oras/internal/errors"
"oras.land/oras/cmd/oras/internal/option"
"oras.land/oras/cmd/oras/internal/output"
"oras.land/oras/internal/contentutil"
"oras.land/oras/internal/descriptor"
"oras.land/oras/internal/listener"
)

type createOptions struct {
option.Common
option.Target

sources []string
extraRefs []string
}

func createCmd() *cobra.Command {
var opts createOptions
cmd := &cobra.Command{
Use: "create [flags] <name>[:<tag[,<tag>][...]] [{<tag>|<digest>}...]",
Short: "[Experimental] Create and push an index from provided manifests",
Long: `[Experimental] Create and push an index from provided manifests. All manifests should be in the same repository
Example - create an index from source manifests tagged 'linux-amd64' and 'linux-arm64', and push without tagging:
oras manifest index create localhost:5000/hello linux-amd64 linux-arm64
Example - create an index from source manifests tagged 'linux-amd64' and 'linux-arm64', and push with the tag 'latest':
oras manifest index create localhost:5000/hello:latest linux-amd64 linux-arm64
Example - create an index from source manifests using both tags and digests, and push with tag 'latest':
oras manifest index create localhost:5000/hello:latest linux-amd64 sha256:xxx
Example - create an index and push it with multiple tags:
oras manifest index create localhost:5000/hello:tag1,tag2,tag3 linux-amd64 linux-arm64 sha256:xxx
`,
Args: oerrors.CheckArgs(argument.AtLeast(1), "the destination index to create."),
PreRunE: func(cmd *cobra.Command, args []string) error {
refs := strings.Split(args[0], ",")
opts.RawReference = refs[0]
opts.extraRefs = refs[1:]
opts.sources = args[1:]
if err := option.Parse(cmd, &opts); err != nil {
return err

Check warning on line 76 in cmd/oras/root/manifest/index/create.go

View check run for this annotation

Codecov / codecov/patch

cmd/oras/root/manifest/index/create.go#L76

Added line #L76 was not covered by tests
}
// check that all tags given by the user are valid tags
allRefs := opts.extraRefs
if opts.Reference != "" {
allRefs = append(opts.extraRefs, opts.Reference)
}
for _, ref := range allRefs {
if !contentutil.IsValidTag(ref) {
return fmt.Errorf("%s is not a valid tag", ref)
}
}
return nil
},
Aliases: []string{"pack"},
RunE: func(cmd *cobra.Command, args []string) error {
return createIndex(cmd, opts)
},
}
option.ApplyFlags(&opts, cmd.Flags())
return oerrors.Command(cmd, &opts.Target)
}

func createIndex(cmd *cobra.Command, opts createOptions) error {
ctx, logger := command.GetLogger(cmd, &opts.Common)
target, err := opts.NewTarget(opts.Common, logger)
if err != nil {
return err

Check warning on line 103 in cmd/oras/root/manifest/index/create.go

View check run for this annotation

Codecov / codecov/patch

cmd/oras/root/manifest/index/create.go#L103

Added line #L103 was not covered by tests
}
manifests, err := resolveSourceManifests(ctx, target, opts)
if err != nil {
return err
}
index := ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2,
},
MediaType: ocispec.MediaTypeImageIndex,
Manifests: manifests,
}
indexBytes, _ := json.Marshal(index)
desc := content.NewDescriptorFromBytes(ocispec.MediaTypeImageIndex, indexBytes)
opts.Println(status.IndexPromptPacked, descriptor.ShortDigest(desc), ocispec.MediaTypeImageIndex)
return pushIndex(ctx, target, desc, indexBytes, opts.Reference, opts.extraRefs, opts.AnnotatedReference(), opts.Printer)
}

func resolveSourceManifests(ctx context.Context, target oras.ReadOnlyTarget, opts createOptions) ([]ocispec.Descriptor, error) {
resolved := []ocispec.Descriptor{}
for _, source := range opts.sources {
opts.Println(status.IndexPromptFetching, source)
desc, content, err := oras.FetchBytes(ctx, target, source, oras.DefaultFetchBytesOptions)
if err != nil {
return nil, fmt.Errorf("could not find the manifest %s: %w", source, err)
}
if !descriptor.IsManifest(desc) {
return nil, fmt.Errorf("%s is not a manifest", source)

Check warning on line 131 in cmd/oras/root/manifest/index/create.go

View check run for this annotation

Codecov / codecov/patch

cmd/oras/root/manifest/index/create.go#L131

Added line #L131 was not covered by tests
}
opts.Println(status.IndexPromptFetched, source)
if descriptor.IsImageManifest(desc) {
desc.Platform, err = getPlatform(ctx, target, content)
if err != nil {
return nil, err

Check warning on line 137 in cmd/oras/root/manifest/index/create.go

View check run for this annotation

Codecov / codecov/patch

cmd/oras/root/manifest/index/create.go#L137

Added line #L137 was not covered by tests
}
}
resolved = append(resolved, desc)
}
return resolved, nil
}

func getPlatform(ctx context.Context, target oras.ReadOnlyTarget, manifestBytes []byte) (*ocispec.Platform, error) {
// extract config descriptor
var manifest ocispec.Manifest
if err := json.Unmarshal(manifestBytes, &manifest); err != nil {
return nil, err

Check warning on line 149 in cmd/oras/root/manifest/index/create.go

View check run for this annotation

Codecov / codecov/patch

cmd/oras/root/manifest/index/create.go#L149

Added line #L149 was not covered by tests
}
// fetch config content
contentBytes, err := content.FetchAll(ctx, target, manifest.Config)
if err != nil {
return nil, err

Check warning on line 154 in cmd/oras/root/manifest/index/create.go

View check run for this annotation

Codecov / codecov/patch

cmd/oras/root/manifest/index/create.go#L154

Added line #L154 was not covered by tests
}
var platform ocispec.Platform
if err := json.Unmarshal(contentBytes, &platform); err != nil || (platform.Architecture == "" && platform.OS == "") {
// ignore if the manifest does not have platform information
return nil, nil

Check warning on line 159 in cmd/oras/root/manifest/index/create.go

View check run for this annotation

Codecov / codecov/patch

cmd/oras/root/manifest/index/create.go#L159

Added line #L159 was not covered by tests
}
return &platform, nil
}

func pushIndex(ctx context.Context, target oras.Target, desc ocispec.Descriptor, content []byte, ref string, extraRefs []string, path string, printer *output.Printer) error {
// push the index
var err error
if ref == "" || contentutil.IsDigest(ref) {
err = target.Push(ctx, desc, bytes.NewReader(content))
} else {
_, err = oras.TagBytes(ctx, target, desc.MediaType, content, ref)
}
if err != nil {
return err

Check warning on line 173 in cmd/oras/root/manifest/index/create.go

View check run for this annotation

Codecov / codecov/patch

cmd/oras/root/manifest/index/create.go#L173

Added line #L173 was not covered by tests
}
printer.Println(status.IndexPromptPushed, path)
if len(extraRefs) != 0 {
handler := display.NewIndexCreateHandler(printer)
tagListener := listener.NewTaggedListener(target, handler.OnTagged)
if _, err = oras.TagBytesN(ctx, tagListener, desc.MediaType, content, extraRefs, oras.DefaultTagBytesNOptions); err != nil {
return err

Check warning on line 180 in cmd/oras/root/manifest/index/create.go

View check run for this annotation

Codecov / codecov/patch

cmd/oras/root/manifest/index/create.go#L180

Added line #L180 was not covered by tests
}
}
return printer.Println("Digest:", desc.Digest)
}
Loading

0 comments on commit e939b85

Please sign in to comment.