Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion cmd/nerdctl/image/image_save.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@
package image

import (
"context"
"fmt"
"os"

"github.com/mattn/go-isatty"
"github.com/spf13/cobra"

"github.com/containerd/log"

"github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
"github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
"github.com/containerd/nerdctl/v2/pkg/api/types"
Expand Down Expand Up @@ -91,7 +94,14 @@ func saveAction(cmd *cobra.Command, args []string) error {
return err
}
output = f
defer f.Close()
defer func() {
if err := f.Sync(); err != nil {
f.Close()
log.G(context.Background()).Error(err)
return
}
f.Close()
}()
} else if out, ok := output.(*os.File); ok && isatty.IsTerminal(out.Fd()) {
return fmt.Errorf("cowardly refusing to save to a terminal. Use the -o flag or redirect")
}
Expand Down
28 changes: 5 additions & 23 deletions pkg/imgutil/load/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"fmt"
"os"
"strings"
"time"

containerd "github.com/containerd/containerd/v2/client"
"github.com/containerd/containerd/v2/core/images"
Expand Down Expand Up @@ -82,35 +83,16 @@ func FromArchive(ctx context.Context, client *containerd.Client, options types.I
storeOpts = append(storeOpts, transferimage.WithPlatforms(platUnpack))
}
storeOpts = append(storeOpts, transferimage.WithUnpack(platUnpack, options.GOptions.Snapshotter))
storeOpts = append(storeOpts, transferimage.WithDigestRef("import", true, true))

var loadedImages []images.Image
pf, done := transferutil.ProgressHandler(ctx, options.Stdout)
storeOpts = append(storeOpts, transferimage.WithNamedPrefix(fmt.Sprintf("import-%s", time.Now().Format("2006-01-02")), true))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It fixes the dash-to-space bug, but untagged archives will now print internal names to users?


pf, done, loadedImages := transferutil.ProgressHandlerLoadImage(ctx, client, beforeSet, options)
err = client.Transfer(ctx,
tarchive.NewImageImportStream(options.Stdin, ""),
transferimage.NewStore("", storeOpts...),
transfer.WithProgress(func(p transfer.Progress) {
if p.Event == "saved" {
if img, err := imageService.Get(ctx, p.Name); err == nil {
if !beforeSet[img.Name] {
loadedImages = append(loadedImages, img)
}
}
}
pf(p)
}),
transfer.WithProgress(pf),
)

done()

if !options.Quiet {
for _, img := range loadedImages {
fmt.Fprintf(options.Stdout, "Loaded image: %s\n", img.Name)
}
}

return loadedImages, err
return *loadedImages, err
}

// FromOCIArchive loads and unpacks the images from the OCI formatted archive at the provided file system path.
Expand Down
127 changes: 126 additions & 1 deletion pkg/transferutil/progress.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,12 @@ import (

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

containerd "github.com/containerd/containerd/v2/client"
"github.com/containerd/containerd/v2/core/images"
"github.com/containerd/containerd/v2/core/transfer"
"github.com/containerd/containerd/v2/pkg/progress"

"github.com/containerd/nerdctl/v2/pkg/api/types"
)

// From https://github.com/containerd/containerd/blob/v2.2.0-rc.0/cmd/ctr/commands/image/pull.go#L240-L473
Expand Down Expand Up @@ -156,6 +160,127 @@ func ProgressHandler(ctx context.Context, out io.Writer) (transfer.ProgressFunc,
return progressFn, done
}

func ProgressHandlerLoadImage(ctx context.Context, client *containerd.Client, beforeSet map[string]bool, options types.ImageLoadOptions) (transfer.ProgressFunc, func(), *[]images.Image) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When client.Transfer finishes, it triggers cancel(), immediately killing the ProgressHandlerLoadImage goroutine. Any delayed saved events will still be dropped?

ctx, cancel := context.WithCancel(ctx)
var (
fw = progress.NewWriter(options.Stdout)
start = time.Now()
statuses = map[string]*progressNode{}
roots = []*progressNode{}
pc = make(chan transfer.Progress, 5)
status string
closeC = make(chan struct{})
loadedImages []images.Image
imagesDisplay []string
)

result := &loadedImages
progressFn := func(p transfer.Progress) {
select {
case pc <- p:
case <-ctx.Done():
}
}

done := func() {
cancel()
<-closeC
if !options.Quiet {
for _, img := range imagesDisplay {
fmt.Fprintf(options.Stdout, "Loaded image: %s\n", img)
}
}
}

go func() {
defer close(closeC)
for {
select {
case p := <-pc:
if p.Name == "" {
status = p.Event
continue
}
if p.Event == "saved" {
if img, err := client.ImageService().Get(ctx, p.Name); err == nil {
if !beforeSet[img.Name] {
loadedImages = append(loadedImages, img)
}
imagesDisplay = append(imagesDisplay, p.Name)
}
}
if node, ok := statuses[p.Name]; !ok {
node = &progressNode{
Progress: p,
root: true,
}
if len(p.Parents) == 0 {
roots = append(roots, node)
} else {
var parents []string
for _, parent := range p.Parents {
pStatus, ok := statuses[parent]
if ok {
parents = append(parents, parent)
pStatus.children = append(pStatus.children, node)
node.root = false
}
}
node.Progress.Parents = parents
if node.root {
roots = append(roots, node)
}
}
statuses[p.Name] = node
} else {
if len(node.Progress.Parents) != len(p.Parents) {
var parents []string
var removeRoot bool
for _, parent := range p.Parents {
pStatus, ok := statuses[parent]
if ok {
parents = append(parents, parent)
var found bool
for _, child := range pStatus.children {
if child.Progress.Name == p.Name {
found = true
break
}
}
if !found {
pStatus.children = append(pStatus.children, node)
}
if node.root {
removeRoot = true
}
node.root = false
}
}
p.Parents = parents
// Check if needs to remove from root
if removeRoot {
for i := range roots {
if roots[i] == node {
roots = append(roots[:i], roots[i+1:]...)
break
}
}
}
}
node.Progress = p
}

displayHierarchy(fw, status, roots, start)
fw.Flush()

case <-ctx.Done():
return
}
}
}()
return progressFn, done, result
}

func displayHierarchy(w io.Writer, status string, roots []*progressNode, start time.Time) {
total := displayNode(w, "", roots)
for _, r := range roots {
Expand Down Expand Up @@ -207,7 +332,7 @@ func displayNode(w io.Writer, prefix string, nodes []*progressNode) int64 {
status.Event,
bar)
default:
fmt.Fprintf(w, "%-40.40s\t%s\t\n",
fmt.Fprintf(w, "%s\t%s\t\n",
name,
status.Event)
}
Expand Down
Loading