Skip to content

Commit

Permalink
refactor: Successor getting with separation of concerns
Browse files Browse the repository at this point in the history
Signed-off-by: Terry Howe <terrylhowe@gmail.com>
  • Loading branch information
TerryHowe committed Aug 9, 2024
1 parent f85f57b commit 0b2cc3b
Show file tree
Hide file tree
Showing 10 changed files with 244 additions and 106 deletions.
20 changes: 16 additions & 4 deletions cmd/oras/internal/display/status/text.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ package status

import (
"context"
"oras.land/oras/internal/graph"
"sync"

"oras.land/oras/cmd/oras/internal/output"

ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2"
"oras.land/oras-go/v2/content"
"oras.land/oras/cmd/oras/internal/output"
)

// TextPushHandler handles text status output for push events.
Expand Down Expand Up @@ -65,9 +65,15 @@ func (ph *TextPushHandler) UpdateCopyOptions(opts *oras.CopyGraphOptions, fetche
}
opts.PostCopy = func(ctx context.Context, desc ocispec.Descriptor) error {
committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle])
if err := output.PrintSuccessorStatus(ctx, desc, fetcher, committed, ph.printer.StatusPrinter(PushPromptSkipped)); err != nil {
successors, err := graph.FilteredSuccessors(ctx, desc, fetcher, DeduplicatedFilter(committed))
if err != nil {
return err
}
for _, successor := range successors {
if err = ph.printer.PrintStatus(successor, PushPromptSkipped); err != nil {
return err
}
}
return ph.printer.PrintStatus(desc, PushPromptUploaded)
}
}
Expand Down Expand Up @@ -149,9 +155,15 @@ func (ch *TextCopyHandler) PreCopy(_ context.Context, desc ocispec.Descriptor) e
// PostCopy implements PostCopy of CopyHandler.
func (ch *TextCopyHandler) PostCopy(ctx context.Context, desc ocispec.Descriptor) error {
ch.committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle])
if err := output.PrintSuccessorStatus(ctx, desc, ch.fetcher, ch.committed, ch.printer.StatusPrinter(copyPromptSkipped)); err != nil {
successors, err := graph.FilteredSuccessors(ctx, desc, ch.fetcher, DeduplicatedFilter(ch.committed))
if err != nil {
return err
}
for _, successor := range successors {
if err = ch.printer.PrintStatus(successor, copyPromptSkipped); err != nil {
return err
}
}
return ch.printer.PrintStatus(desc, copyPromptCopied)
}

Expand Down
131 changes: 131 additions & 0 deletions cmd/oras/internal/display/status/text_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
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 status

import (
"bytes"
"context"
"encoding/json"
"fmt"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/content/memory"
"oras.land/oras/cmd/oras/internal/output"
"oras.land/oras/internal/testutils"
"os"
"strings"
"testing"
)

var (
ctx context.Context
builder *strings.Builder
printer *output.Printer
bogus ocispec.Descriptor
memStore *memory.Store
memDesc ocispec.Descriptor
manifestDesc ocispec.Descriptor
)

func TestMain(m *testing.M) {
// memory store for testing
memStore = memory.New()
content := []byte("test")
r := bytes.NewReader(content)
memDesc = ocispec.Descriptor{
MediaType: "application/octet-stream",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
if err := memStore.Push(context.Background(), memDesc, r); err != nil {
fmt.Println("Setup failed:", err)
os.Exit(1)
}
if err := memStore.Tag(context.Background(), memDesc, memDesc.Digest.String()); err != nil {
fmt.Println("Setup failed:", err)
os.Exit(1)
}

layer1Desc := memDesc
layer1Desc.Annotations = map[string]string{ocispec.AnnotationTitle: "layer1"}
layer2Desc := memDesc
layer2Desc.Annotations = map[string]string{ocispec.AnnotationTitle: "layer2"}
manifest := ocispec.Manifest{
MediaType: ocispec.MediaTypeImageManifest,
Layers: []ocispec.Descriptor{layer1Desc, layer2Desc},
Config: memDesc,
}
manifestContent, err := json.Marshal(&manifest)
if err != nil {
fmt.Println("Setup failed:", err)
os.Exit(1)
}
manifestDesc = ocispec.Descriptor{
MediaType: manifest.MediaType,
Size: int64(len(manifestContent)),
Digest: digest.FromBytes(manifestContent),
}
if err := memStore.Push(context.Background(), manifestDesc, strings.NewReader(string(manifestContent))); err != nil {
fmt.Println("Setup failed:", err)
os.Exit(1)
}
if err := memStore.Tag(context.Background(), memDesc, memDesc.Digest.String()); err != nil {
fmt.Println("Setup failed:", err)
os.Exit(1)
}

ctx = context.Background()
builder = &strings.Builder{}
printer = output.NewPrinter(builder, os.Stderr, false)
bogus = ocispec.Descriptor{MediaType: ocispec.MediaTypeImageManifest}
m.Run()
}

func TestTextCopyHandler_OnMounted(t *testing.T) {
fetcher := testutils.NewMockFetcher(t)
ch := NewTextCopyHandler(printer, fetcher.Fetcher)
if ch.OnMounted(ctx, fetcher.OciImage) != nil {
t.Error("OnMounted() should not return an error")
}

}

func TestTextCopyHandler_OnCopySkipped(t *testing.T) {
fetcher := testutils.NewMockFetcher(t)
ch := NewTextCopyHandler(printer, fetcher.Fetcher)
if ch.OnCopySkipped(ctx, fetcher.OciImage) != nil {
t.Error("OnCopySkipped() should not return an error")
}
}

func TestTextCopyHandler_PostCopy(t *testing.T) {
fetcher := testutils.NewMockFetcher(t)
ch := NewTextCopyHandler(printer, fetcher.Fetcher)
if ch.PostCopy(ctx, fetcher.OciImage) != nil {
t.Error("PostCopy() should not return an error")
}
if ch.PostCopy(ctx, bogus) == nil {
t.Error("PostCopy() should return an error")
}
}

func TestTextCopyHandler_PreCopy(t *testing.T) {
fetcher := testutils.NewMockFetcher(t)
ch := NewTextCopyHandler(printer, fetcher.Fetcher)
if ch.PreCopy(ctx, fetcher.OciImage) != nil {
t.Error("PreCopy() should not return an error")
}
}
17 changes: 12 additions & 5 deletions cmd/oras/internal/display/status/tty.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,10 @@ package status

import (
"context"
"oras.land/oras/internal/graph"
"os"
"sync"

"oras.land/oras/cmd/oras/internal/output"

ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2"
"oras.land/oras-go/v2/content"
Expand Down Expand Up @@ -70,9 +69,17 @@ func (ph *TTYPushHandler) UpdateCopyOptions(opts *oras.CopyGraphOptions, fetcher
}
opts.PostCopy = func(ctx context.Context, desc ocispec.Descriptor) error {
committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle])
return output.PrintSuccessorStatus(ctx, desc, fetcher, committed, func(d ocispec.Descriptor) error {
return ph.tracked.Prompt(d, PushPromptSkipped)
})
successors, err := graph.FilteredSuccessors(ctx, desc, fetcher, DeduplicatedFilter(committed))
if err != nil {
return err
}
for _, successor := range successors {
err = ph.tracked.Prompt(successor, PushPromptSkipped)
if err != nil {
return err
}
}
return nil
}
}

Expand Down
62 changes: 0 additions & 62 deletions cmd/oras/internal/display/status/tty_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,74 +16,12 @@ limitations under the License.
package status

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

"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/content/memory"
)

var (
memStore *memory.Store
memDesc ocispec.Descriptor
manifestDesc ocispec.Descriptor
)

func TestMain(m *testing.M) {
// memory store for testing
memStore = memory.New()
content := []byte("test")
r := bytes.NewReader(content)
memDesc = ocispec.Descriptor{
MediaType: "application/octet-stream",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
if err := memStore.Push(context.Background(), memDesc, r); err != nil {
fmt.Println("Setup failed:", err)
os.Exit(1)
}
if err := memStore.Tag(context.Background(), memDesc, memDesc.Digest.String()); err != nil {
fmt.Println("Setup failed:", err)
os.Exit(1)
}

layer1Desc := memDesc
layer1Desc.Annotations = map[string]string{ocispec.AnnotationTitle: "layer1"}
layer2Desc := memDesc
layer2Desc.Annotations = map[string]string{ocispec.AnnotationTitle: "layer2"}
manifest := ocispec.Manifest{
MediaType: ocispec.MediaTypeImageManifest,
Layers: []ocispec.Descriptor{layer1Desc, layer2Desc},
Config: memDesc,
}
manifestContent, err := json.Marshal(&manifest)
if err != nil {
fmt.Println("Setup failed:", err)
os.Exit(1)
}
manifestDesc = ocispec.Descriptor{
MediaType: manifest.MediaType,
Size: int64(len(manifestContent)),
Digest: digest.FromBytes(manifestContent),
}
if err := memStore.Push(context.Background(), manifestDesc, strings.NewReader(string(manifestContent))); err != nil {
fmt.Println("Setup failed:", err)
os.Exit(1)
}
if err := memStore.Tag(context.Background(), memDesc, memDesc.Digest.String()); err != nil {
fmt.Println("Setup failed:", err)
os.Exit(1)
}
m.Run()
}

func TestTTYPushHandler_OnFileLoading(t *testing.T) {
ph := NewTTYPushHandler(os.Stdout)
if ph.OnFileLoading("test") != nil {
Expand Down
16 changes: 16 additions & 0 deletions cmd/oras/internal/display/status/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ limitations under the License.

package status

import (
"sync"

ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)

// Prompts for pull events.
const (
PullPromptDownloading = "Downloading"
Expand All @@ -41,3 +47,13 @@ const (
copyPromptSkipped = "Skipped"
copyPromptMounted = "Mounted"
)

// DeduplicatedFilter filters out deduplicated descriptors.
func DeduplicatedFilter(committed *sync.Map) func(desc ocispec.Descriptor) bool {
return func(desc ocispec.Descriptor) bool {
name := desc.Annotations[ocispec.AnnotationTitle]
v, ok := committed.Load(desc.Digest.String())
// committed but not printed == deduplicated
return ok && v != name
}
}
30 changes: 0 additions & 30 deletions cmd/oras/internal/output/print.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,15 @@ limitations under the License.
package output

import (
"context"
"fmt"
"io"
"sync"

"oras.land/oras/internal/descriptor"

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

// PrintFunc is the function type returned by StatusPrinter.
type PrintFunc func(ocispec.Descriptor) error

// Printer prints for status handlers.
type Printer struct {
out io.Writer
Expand Down Expand Up @@ -92,28 +87,3 @@ func (p *Printer) PrintStatus(desc ocispec.Descriptor, status string) error {
}
return p.Println(status, descriptor.ShortDigest(desc), name)
}

// StatusPrinter returns a tracking function for transfer status.
func (p *Printer) StatusPrinter(status string) PrintFunc {
return func(desc ocispec.Descriptor) error {
return p.PrintStatus(desc, status)
}
}

// PrintSuccessorStatus prints transfer status of successors.
func PrintSuccessorStatus(ctx context.Context, desc ocispec.Descriptor, fetcher content.Fetcher, committed *sync.Map, print PrintFunc) error {
successors, err := content.Successors(ctx, fetcher, desc)
if err != nil {
return err
}
for _, s := range successors {
name := s.Annotations[ocispec.AnnotationTitle]
if v, ok := committed.Load(s.Digest.String()); ok && v != name {
// Reprint status for deduplicated content
if err := print(s); err != nil {
return err
}
}
}
return nil
}
14 changes: 10 additions & 4 deletions cmd/oras/root/cp.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ import (
"oras.land/oras/cmd/oras/internal/display/status/track"
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/docker"
"oras.land/oras/internal/graph"
"oras.land/oras/internal/listener"
Expand Down Expand Up @@ -198,9 +197,16 @@ func doCopy(ctx context.Context, copyHandler status.CopyHandler, src oras.ReadOn
}
extendedCopyOptions.PostCopy = func(ctx context.Context, desc ocispec.Descriptor) error {
committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle])
return output.PrintSuccessorStatus(ctx, desc, tracked, committed, func(desc ocispec.Descriptor) error {
return tracked.Prompt(desc, promptSkipped)
})
successors, err := graph.FilteredSuccessors(ctx, desc, tracked, status.DeduplicatedFilter(committed))
if err != nil {
return err
}
for _, successor := range successors {
if err = tracked.Prompt(successor, promptSkipped); err != nil {
return err
}
}
return nil
}
extendedCopyOptions.OnMounted = func(ctx context.Context, desc ocispec.Descriptor) error {
committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle])
Expand Down
Loading

0 comments on commit 0b2cc3b

Please sign in to comment.