Skip to content

Commit

Permalink
Merge branch 'main' into feat/resolve
Browse files Browse the repository at this point in the history
  • Loading branch information
sajayantony authored Nov 14, 2023
2 parents e5fe691 + d7c71e6 commit 260b488
Show file tree
Hide file tree
Showing 22 changed files with 275 additions and 85 deletions.
2 changes: 1 addition & 1 deletion cmd/oras/internal/display/console/console.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ func (c *Console) NewRow() {
func (c *Console) OutputTo(upCnt uint, str string) {
_, _ = c.Write([]byte(Restore))
_, _ = c.Write([]byte(aec.PreviousLine(upCnt).Apply(str)))
_, _ = c.Write([]byte(" "))
_, _ = c.Write([]byte("\n"))
_, _ = c.Write([]byte(aec.EraseLine(aec.EraseModes.Tail).String()))
}

Expand Down
15 changes: 9 additions & 6 deletions cmd/oras/internal/display/progress/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ type status struct {
func newStatus() *status {
return &status{
offset: -1,
total: humanize.ToBytes(0),
lastRenderTime: time.Now(),
}
}
Expand Down Expand Up @@ -99,9 +100,6 @@ func (s *status) String(width int) (string, string) {
// todo: doesn't support multiline prompt
total := uint64(s.descriptor.Size)
var percent float64
if s.offset >= 0 {
percent = float64(s.offset) / float64(total)
}

name := s.descriptor.Annotations["org.opencontainers.image.title"]
if name == "" {
Expand All @@ -112,10 +110,15 @@ func (s *status) String(width int) (string, string) {
// mark(1) bar(22) speed(8) action(<=11) name(<=126) size_per_size(<=13) percent(8) time(>=6)
// └─ digest(72)
var offset string
switch percent {
case 1: // 100%, show exact size
switch s.done {
case true: // 100%, show exact size
offset = fmt.Sprint(s.total.Size)
percent = 1
default: // 0% ~ 99%, show 2-digit precision
if total != 0 && s.offset >= 0 {
// percentage calculatable
percent = float64(s.offset) / float64(total)
}
offset = fmt.Sprintf("%.2f", humanize.RoundTo(s.total.Size*percent))
}
right := fmt.Sprintf(" %s/%s %6.2f%% %6s", offset, s.total, percent*100, s.durationString())
Expand All @@ -131,7 +134,7 @@ func (s *status) String(width int) (string, string) {
// bar + wrapper(2) + space(1) + speed + "/s"(2) + wrapper(2) = len(bar) + len(speed) + 7
lenLeft = barLength + speedLength + 7
} else {
left = fmt.Sprintf(" %s %s", s.prompt, name)
left = fmt.Sprintf(" %s %s", s.prompt, name)
}
// mark(1) + space(1) + prompt + space(1) + name = len(prompt) + len(name) + 3
lenLeft += utf8.RuneCountInString(s.prompt) + utf8.RuneCountInString(name) + 3
Expand Down
38 changes: 36 additions & 2 deletions cmd/oras/internal/display/progress/status_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,19 +54,53 @@ func Test_status_String(t *testing.T) {
if err := testutils.OrderedMatch(statusStr+digestStr, " [\x1b[7m\x1b[0m....................]", s.prompt, "application/v.", "0.00/2 B", "0.00%", s.descriptor.Digest.String()); err != nil {
t.Error(err)
}

// done
s.Update(&status{
endTime: time.Now(),
offset: s.descriptor.Size,
descriptor: s.descriptor,
})
statusStr, digestStr = s.String(120)
if err := testutils.OrderedMatch(statusStr+digestStr, "", s.prompt, s.descriptor.MediaType, "2/2 B", "100.00%", s.descriptor.Digest.String()); err != nil {
if err := testutils.OrderedMatch(statusStr+digestStr, "", s.prompt, s.descriptor.MediaType, "2/2 B", "100.00%", s.descriptor.Digest.String()); err != nil {
t.Error(err)
}
}

func Test_status_String_zeroWitdth(t *testing.T) {
// zero status and progress
s := newStatus()
if status, digest := s.String(console.MinWidth); status != zeroStatus || digest != zeroDigest {
t.Errorf("status.String() = %v, %v, want %v, %v", status, digest, zeroStatus, zeroDigest)
}

// not done
s.Update(&status{
prompt: "test",
descriptor: ocispec.Descriptor{
MediaType: "application/vnd.oci.empty.oras.test.v1+json",
Size: 0,
Digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
},
startTime: time.Now().Add(-time.Minute),
offset: 0,
total: humanize.ToBytes(0),
})
// not done
statusStr, digestStr := s.String(120)
if err := testutils.OrderedMatch(statusStr+digestStr, " [\x1b[7m\x1b[0m....................]", s.prompt, s.descriptor.MediaType, "0.00/0 B", "0.00%", s.descriptor.Digest.String()); err != nil {
t.Error(err)
}
// done
s.Update(&status{
endTime: time.Now(),
offset: s.descriptor.Size,
descriptor: s.descriptor,
})
statusStr, digestStr = s.String(120)
if err := testutils.OrderedMatch(statusStr+digestStr, "✓", s.prompt, s.descriptor.MediaType, "0/0 B", "100.00%", s.descriptor.Digest.String()); err != nil {
t.Error(err)
}
}
func Test_status_durationString(t *testing.T) {
// zero duration
s := newStatus()
Expand Down
10 changes: 5 additions & 5 deletions cmd/oras/internal/option/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ package option

import (
"context"
"errors"
"os"

"github.com/sirupsen/logrus"
Expand All @@ -37,7 +36,7 @@ type Common struct {

// ApplyFlags applies flags to a command flag set.
func (opts *Common) ApplyFlags(fs *pflag.FlagSet) {
fs.BoolVarP(&opts.Debug, "debug", "d", false, "debug mode")
fs.BoolVarP(&opts.Debug, "debug", "d", false, "output debug logs (implies --no-tty)")
fs.BoolVarP(&opts.Verbose, "verbose", "v", false, "verbose output")
fs.BoolVarP(&opts.noTTY, "no-tty", "", false, "[Preview] do not show progress output")
}
Expand All @@ -55,11 +54,12 @@ func (opts *Common) Parse() error {

// parseTTY gets target options from user input.
func (opts *Common) parseTTY(f *os.File) error {
if !opts.noTTY && term.IsTerminal(int(f.Fd())) {
if !opts.noTTY {
if opts.Debug {
return errors.New("cannot use --debug, add --no-tty to suppress terminal output")
opts.noTTY = true
} else if term.IsTerminal(int(f.Fd())) {
opts.TTY = f
}
opts.TTY = f
}
return nil
}
7 changes: 5 additions & 2 deletions cmd/oras/internal/option/common_unix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ func TestCommon_parseTTY(t *testing.T) {

// --debug
opts.Debug = true
if err := opts.parseTTY(device); err == nil {
t.Error("expected error when debug is set with TTY output")
if err := opts.parseTTY(device); err != nil {
t.Errorf("unexpected error with --debug: %v", err)
}
if !opts.noTTY {
t.Errorf("expected --no-tty to be true with --debug")
}
}
14 changes: 13 additions & 1 deletion cmd/oras/internal/option/target.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import (
"context"
"errors"
"fmt"
"io"
"io/fs"
"os"
"strings"
"sync"
Expand Down Expand Up @@ -154,12 +156,22 @@ func (opts *Target) NewReadonlyTarget(ctx context.Context, common Common, logger
}
info, err := os.Stat(opts.Path)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return nil, fmt.Errorf("invalid argument %q: failed to find path %q: %w", opts.RawReference, opts.Path, err)
}
return nil, err
}
if info.IsDir() {
return oci.NewFromFS(ctx, os.DirFS(opts.Path))
}
return oci.NewFromTar(ctx, opts.Path)
store, err := oci.NewFromTar(ctx, opts.Path)
if err != nil {
if errors.Is(err, io.ErrUnexpectedEOF) {
return nil, fmt.Errorf("%q does not look like a tar archive: %w", opts.Path, err)
}
return nil, err
}
return store, nil
case TargetTypeRemote:
repo, err := opts.NewRepository(opts.RawReference, common, logger)
if err != nil {
Expand Down
21 changes: 14 additions & 7 deletions cmd/oras/root/cp.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,38 +154,45 @@ func doCopy(ctx context.Context, src oras.ReadOnlyGraphTarget, dst oras.GraphTar
return graph.Referrers(ctx, src, desc, "")
}

const (
promptExists = "Exists "
promptCopying = "Copying"
promptCopied = "Copied "
promptSkipped = "Skipped"
)

if opts.TTY == nil {
// none TTY output
extendedCopyOptions.OnCopySkipped = func(ctx context.Context, desc ocispec.Descriptor) error {
committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle])
return display.PrintStatus(desc, "Exists ", opts.Verbose)
return display.PrintStatus(desc, promptExists, opts.Verbose)
}
extendedCopyOptions.PreCopy = func(ctx context.Context, desc ocispec.Descriptor) error {
return display.PrintStatus(desc, "Copying", opts.Verbose)
return display.PrintStatus(desc, promptCopying, opts.Verbose)
}
extendedCopyOptions.PostCopy = func(ctx context.Context, desc ocispec.Descriptor) error {
committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle])
if err := display.PrintSuccessorStatus(ctx, desc, dst, committed, display.StatusPrinter("Skipped", opts.Verbose)); err != nil {
if err := display.PrintSuccessorStatus(ctx, desc, dst, committed, display.StatusPrinter(promptSkipped, opts.Verbose)); err != nil {
return err
}
return display.PrintStatus(desc, "Copied ", opts.Verbose)
return display.PrintStatus(desc, promptCopied, opts.Verbose)
}
} else {
// TTY output
tracked, err := track.NewTarget(dst, "Copying ", "Copied ", opts.TTY)
tracked, err := track.NewTarget(dst, promptCopying, promptCopied, opts.TTY)
if err != nil {
return ocispec.Descriptor{}, err
}
defer tracked.Close()
dst = tracked
extendedCopyOptions.OnCopySkipped = func(ctx context.Context, desc ocispec.Descriptor) error {
committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle])
return tracked.Prompt(desc, "Exists ")
return tracked.Prompt(desc, promptExists)
}
extendedCopyOptions.PostCopy = func(ctx context.Context, desc ocispec.Descriptor) error {
committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle])
return display.PrintSuccessorStatus(ctx, desc, tracked, committed, func(desc ocispec.Descriptor) error {
return tracked.Prompt(desc, "Skipped")
return tracked.Prompt(desc, promptSkipped)
})
}
}
Expand Down
58 changes: 39 additions & 19 deletions cmd/oras/root/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"fmt"
"io"
"sync"
"sync/atomic"

ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -130,18 +131,22 @@ func runPull(ctx context.Context, opts pullOptions) error {
dst.AllowPathTraversalOnWrite = opts.PathTraversal
dst.DisableOverwrite = opts.KeepOldFiles

desc, pulledEmpty, err := doPull(ctx, src, dst, copyOptions, &opts)
desc, layerSkipped, err := doPull(ctx, src, dst, copyOptions, &opts)
if err != nil {
if errors.Is(err, file.ErrPathTraversalDisallowed) {
err = fmt.Errorf("%s: %w", "use flag --allow-path-traversal to allow insecurely pulling files outside of working directory", err)
}
return err
}
if pulledEmpty {
fmt.Println("Downloaded empty artifact")

// suggest oras copy for pulling layers without annotation
if layerSkipped {
fmt.Printf("Skipped pulling layers without file name in %q\n", ocispec.AnnotationTitle)
fmt.Printf("Use 'oras copy %s --to-oci-layout <layout-dir>' to pull all layers.\n", opts.RawReference)
} else {
fmt.Println("Pulled", opts.AnnotatedReference())
fmt.Println("Digest:", desc.Digest)
}
fmt.Println("Pulled", opts.AnnotatedReference())
fmt.Println("Digest:", desc.Digest)
return nil
}

Expand All @@ -155,15 +160,24 @@ func doPull(ctx context.Context, src oras.ReadOnlyTarget, dst oras.GraphTarget,
}
}

const (
promptDownloading = "Downloading"
promptPulled = "Pulled "
promptProcessing = "Processing "
promptSkipped = "Skipped "
promptRestored = "Restored "
promptDownloaded = "Downloaded "
)

var tracked track.GraphTarget
dst, tracked, err = getTrackedTarget(dst, po.TTY, "Downloading", "Downloaded ")
dst, tracked, err = getTrackedTarget(dst, po.TTY, "Downloading", "Pulled ")
if err != nil {
return ocispec.Descriptor{}, false, err
}
if tracked != nil {
defer tracked.Close()
}

var layerSkipped atomic.Bool
var printed sync.Map
var getConfigOnce sync.Once
opts.FindSuccessors = func(ctx context.Context, fetcher content.Fetcher, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
Expand All @@ -173,7 +187,7 @@ func doPull(ctx context.Context, src oras.ReadOnlyTarget, dst oras.GraphTarget,
}
if po.TTY == nil {
// none TTY, print status log for first-time fetching
if err := display.PrintStatus(target, "Downloading", po.Verbose); err != nil {
if err := display.PrintStatus(target, promptDownloading, po.Verbose); err != nil {
return nil, err
}
}
Expand All @@ -188,7 +202,7 @@ func doPull(ctx context.Context, src oras.ReadOnlyTarget, dst oras.GraphTarget,
}()
if po.TTY == nil {
// none TTY, add logs for processing manifest
return rc, display.PrintStatus(target, "Processing ", po.Verbose)
return rc, display.PrintStatus(target, promptProcessing, po.Verbose)
}
return rc, nil
})
Expand All @@ -209,19 +223,29 @@ func doPull(ctx context.Context, src oras.ReadOnlyTarget, dst oras.GraphTarget,
config.Annotations[ocispec.AnnotationTitle] = configPath
}
})
nodes = append(nodes, *config)
if config.Size != ocispec.DescriptorEmptyJSON.Size || config.Digest != ocispec.DescriptorEmptyJSON.Digest || config.Annotations[ocispec.AnnotationTitle] != "" {
nodes = append(nodes, *config)
}
}

var ret []ocispec.Descriptor
for _, s := range nodes {
if s.Annotations[ocispec.AnnotationTitle] == "" {
if content.Equal(s, ocispec.DescriptorEmptyJSON) {
// empty layer
continue
}
if s.Annotations[ocispec.AnnotationTitle] == "" {
// unnamed layers are skipped
layerSkipped.Store(true)
}
ss, err := content.Successors(ctx, fetcher, s)
if err != nil {
return nil, err
}
if len(ss) == 0 {
// skip s if it is unnamed AND has no successors.
if err := printOnce(&printed, s, "Skipped ", po.Verbose, tracked); err != nil {
if err := printOnce(&printed, s, promptSkipped, po.Verbose, tracked); err != nil {
return nil, err
}
continue
Expand All @@ -233,14 +257,13 @@ func doPull(ctx context.Context, src oras.ReadOnlyTarget, dst oras.GraphTarget,
return ret, nil
}

pulledEmpty := true
opts.PreCopy = func(ctx context.Context, desc ocispec.Descriptor) error {
if _, ok := printed.LoadOrStore(generateContentKey(desc), true); ok {
return nil
}
if po.TTY == nil {
// none TTY, print status log for downloading
return display.PrintStatus(desc, "Downloading", po.Verbose)
return display.PrintStatus(desc, promptDownloading, po.Verbose)
}
// TTY
return nil
Expand All @@ -253,7 +276,7 @@ func doPull(ctx context.Context, src oras.ReadOnlyTarget, dst oras.GraphTarget,
}
for _, s := range successors {
if _, ok := s.Annotations[ocispec.AnnotationTitle]; ok {
if err := printOnce(&printed, s, "Restored ", po.Verbose, tracked); err != nil {
if err := printOnce(&printed, s, promptRestored, po.Verbose, tracked); err != nil {
return err
}
}
Expand All @@ -264,17 +287,14 @@ func doPull(ctx context.Context, src oras.ReadOnlyTarget, dst oras.GraphTarget,
return nil
}
name = desc.MediaType
} else {
// named content downloaded
pulledEmpty = false
}
printed.Store(generateContentKey(desc), true)
return display.Print("Downloaded ", display.ShortDigest(desc), name)
return display.Print(promptDownloaded, display.ShortDigest(desc), name)
}

// Copy
desc, err := oras.Copy(ctx, src, po.Reference, dst, po.Reference, opts)
return desc, pulledEmpty, err
return desc, layerSkipped.Load(), err
}

// generateContentKey generates a unique key for each content descriptor, using
Expand Down
Loading

0 comments on commit 260b488

Please sign in to comment.