diff --git a/cmd/ctr/app/main.go b/cmd/ctr/app/main.go index 6a4ad2a8..594fff1c 100644 --- a/cmd/ctr/app/main.go +++ b/cmd/ctr/app/main.go @@ -20,6 +20,10 @@ import ( "fmt" "io" + "github.com/containerd/log" + "github.com/urfave/cli/v2" + "google.golang.org/grpc/grpclog" + "github.com/containerd/containerd/v2/cmd/ctr/commands/content" "github.com/containerd/containerd/v2/cmd/ctr/commands/events" "github.com/containerd/containerd/v2/cmd/ctr/commands/install" @@ -37,9 +41,6 @@ import ( "github.com/containerd/imgcrypt/cmd/ctr/commands/containers" "github.com/containerd/imgcrypt/cmd/ctr/commands/images" "github.com/containerd/imgcrypt/cmd/ctr/commands/run" - "github.com/sirupsen/logrus" - "github.com/urfave/cli/v2" - "google.golang.org/grpc/grpclog" ) var extraCmds = []*cli.Command{} @@ -48,8 +49,25 @@ func init() { // Discard grpc logs so that they don't mess with our stdio grpclog.SetLoggerV2(grpclog.NewLoggerV2(io.Discard, io.Discard, io.Discard)) - cli.VersionPrinter = func(c *cli.Context) { - fmt.Println(c.App.Name, version.Package, c.App.Version) + cli.VersionPrinter = func(cliContext *cli.Context) { + fmt.Println(cliContext.App.Name, version.Package, cliContext.App.Version) + } + + // Override the default flag descriptions for '--version' and '--help' + // to align with other flags and start with uppercase. + cli.VersionFlag = &cli.BoolFlag{ + Name: "version", + Aliases: []string{"v"}, + Usage: "Print the version", + + DisableDefaultText: true, + } + cli.HelpFlag = &cli.BoolFlag{ + Name: "help", + Aliases: []string{"h"}, + Usage: "Show help", + + DisableDefaultText: true, } } @@ -77,27 +95,27 @@ containerd CLI app.Flags = []cli.Flag{ &cli.BoolFlag{ Name: "debug", - Usage: "enable debug output in logs", + Usage: "Enable debug output in logs", }, &cli.StringFlag{ Name: "address", Aliases: []string{"a"}, - Usage: "address for containerd's GRPC server", + Usage: "Address for containerd's GRPC server", Value: defaults.DefaultAddress, EnvVars: []string{"CONTAINERD_ADDRESS"}, }, &cli.DurationFlag{ Name: "timeout", - Usage: "total timeout for ctr commands", + Usage: "Total timeout for ctr commands", }, &cli.DurationFlag{ Name: "connect-timeout", - Usage: "timeout for connecting to containerd", + Usage: "Timeout for connecting to containerd", }, &cli.StringFlag{ Name: "namespace", Aliases: []string{"n"}, - Usage: "namespace to use with commands", + Usage: "Namespace to use with commands", Value: namespaces.Default, EnvVars: []string{namespaces.NamespaceEnvVar}, }, @@ -118,9 +136,9 @@ containerd CLI install.Command, ociCmd.Command, }, extraCmds...) - app.Before = func(context *cli.Context) error { - if context.Bool("debug") { - logrus.SetLevel(logrus.DebugLevel) + app.Before = func(cliContext *cli.Context) error { + if cliContext.Bool("debug") { + return log.SetLevel("debug") } return nil } diff --git a/cmd/ctr/app/main_unix.go b/cmd/ctr/app/main_unix.go index d6a9e1ab..ab004e9e 100644 --- a/cmd/ctr/app/main_unix.go +++ b/cmd/ctr/app/main_unix.go @@ -1,5 +1,4 @@ //go:build !windows -// +build !windows /* Copyright The containerd Authors. diff --git a/cmd/ctr/commands/commands.go b/cmd/ctr/commands/commands.go index 48bc060f..76d88789 100644 --- a/cmd/ctr/commands/commands.go +++ b/cmd/ctr/commands/commands.go @@ -23,16 +23,219 @@ import ( "path/filepath" "strings" + "github.com/containerd/containerd/v2/defaults" "github.com/containerd/containerd/v2/pkg/atomicfile" "github.com/urfave/cli/v2" ) +var ( + // SnapshotterFlags are cli flags specifying snapshotter names + SnapshotterFlags = []cli.Flag{ + &cli.StringFlag{ + Name: "snapshotter", + Usage: "Snapshotter name. Empty value stands for the default value.", + EnvVars: []string{"CONTAINERD_SNAPSHOTTER"}, + }, + } + + // SnapshotterLabels are cli flags specifying labels which will be added to the new snapshot for container. + SnapshotterLabels = &cli.StringSliceFlag{ + Name: "snapshotter-label", + Usage: "Labels added to the new snapshot for this container.", + } + + // LabelFlag is a cli flag specifying labels + LabelFlag = &cli.StringSliceFlag{ + Name: "label", + Usage: "Labels to attach to the image", + } + + // RegistryFlags are cli flags specifying registry options + RegistryFlags = []cli.Flag{ + &cli.BoolFlag{ + Name: "skip-verify", + Aliases: []string{"k"}, + Usage: "Skip SSL certificate validation", + }, + &cli.BoolFlag{ + Name: "plain-http", + Usage: "Allow connections using plain HTTP", + }, + &cli.StringFlag{ + Name: "user", + Aliases: []string{"u"}, + Usage: "User[:password] Registry user and password", + }, + &cli.StringFlag{ + Name: "refresh", + Usage: "Refresh token for authorization server", + }, + &cli.StringFlag{ + Name: "hosts-dir", + // compatible with "/etc/docker/certs.d" + Usage: "Custom hosts configuration directory", + }, + &cli.StringFlag{ + Name: "tlscacert", + Usage: "Path to TLS root CA", + }, + &cli.StringFlag{ + Name: "tlscert", + Usage: "Path to TLS client certificate", + }, + &cli.StringFlag{ + Name: "tlskey", + Usage: "Path to TLS client key", + }, + &cli.BoolFlag{ + Name: "http-dump", + Usage: "Dump all HTTP request/responses when interacting with container registry", + }, + &cli.BoolFlag{ + Name: "http-trace", + Usage: "Enable HTTP tracing for registry interactions", + }, + } + + // RuntimeFlags are cli flags specifying runtime + RuntimeFlags = []cli.Flag{ + &cli.StringFlag{ + Name: "runtime", + Usage: "Runtime name or absolute path to runtime binary", + Value: defaults.DefaultRuntime, + }, + &cli.StringFlag{ + Name: "runtime-config-path", + Usage: "Optional runtime config path", + }, + } + + // ContainerFlags are cli flags specifying container options + ContainerFlags = []cli.Flag{ + &cli.StringFlag{ + Name: "config", + Aliases: []string{"c"}, + Usage: "Path to the runtime-specific spec config file", + }, + &cli.StringFlag{ + Name: "cwd", + Usage: "Specify the working directory of the process", + }, + &cli.StringSliceFlag{ + Name: "env", + Usage: "Specify additional container environment variables (e.g. FOO=bar)", + }, + &cli.StringFlag{ + Name: "env-file", + Usage: "Specify additional container environment variables in a file(e.g. FOO=bar, one per line)", + }, + &cli.StringSliceFlag{ + Name: "label", + Usage: "Specify additional labels (e.g. foo=bar)", + }, + &cli.StringSliceFlag{ + Name: "annotation", + Usage: "Specify additional OCI annotations (e.g. foo=bar)", + }, + &cli.StringSliceFlag{ + Name: "mount", + Usage: "Specify additional container mount (e.g. type=bind,src=/tmp,dst=/host,options=rbind:ro)", + }, + &cli.BoolFlag{ + Name: "net-host", + Usage: "Enable host networking for the container", + }, + &cli.BoolFlag{ + Name: "privileged", + Usage: "Run privileged container", + }, + &cli.BoolFlag{ + Name: "read-only", + Usage: "Set the containers filesystem as readonly", + }, + &cli.StringFlag{ + Name: "sandbox", + Usage: "Create the container in the given sandbox", + }, + &cli.BoolFlag{ + Name: "tty", + Aliases: []string{"t"}, + Usage: "Allocate a TTY for the container", + }, + &cli.StringSliceFlag{ + Name: "with-ns", + Usage: "Specify existing Linux namespaces to join at container runtime (format ':')", + }, + &cli.StringFlag{ + Name: "pid-file", + Usage: "File path to write the task's pid", + }, + &cli.IntSliceFlag{ + Name: "gpus", + Usage: "Add gpus to the container", + }, + &cli.BoolFlag{ + Name: "allow-new-privs", + Usage: "Turn off OCI spec's NoNewPrivileges feature flag", + }, + &cli.Uint64Flag{ + Name: "memory-limit", + Usage: "Memory limit (in bytes) for the container", + }, + &cli.StringSliceFlag{ + Name: "cap-add", + Usage: "Add Linux capabilities (Set capabilities with 'CAP_' prefix)", + }, + &cli.StringSliceFlag{ + Name: "cap-drop", + Usage: "Drop Linux capabilities (Set capabilities with 'CAP_' prefix)", + }, + &cli.BoolFlag{ + Name: "seccomp", + Usage: "Enable the default seccomp profile", + }, + &cli.StringFlag{ + Name: "seccomp-profile", + Usage: "File path to custom seccomp profile. seccomp must be set to true, before using seccomp-profile", + }, + &cli.StringFlag{ + Name: "apparmor-default-profile", + Usage: "Enable AppArmor with the default profile with the specified name, e.g. \"cri-containerd.apparmor.d\"", + }, + &cli.StringFlag{ + Name: "apparmor-profile", + Usage: "Enable AppArmor with an existing custom profile", + }, + &cli.StringFlag{ + Name: "blockio-config-file", + Usage: "File path to blockio class definitions. By default class definitions are not loaded.", + }, + &cli.StringFlag{ + Name: "blockio-class", + Usage: "Name of the blockio class to associate the container with", + }, + &cli.StringFlag{ + Name: "rdt-class", + Usage: "Name of the RDT class to associate the container with. Specifies a Class of Service (CLOS) for cache and memory bandwidth management.", + }, + &cli.StringFlag{ + Name: "hostname", + Usage: "Set the container's host name", + }, + &cli.StringFlag{ + Name: "user", + Aliases: []string{"u"}, + Usage: "Username or user id, group optional (format: [:])", + }, + } +) + // ObjectWithLabelArgs returns the first arg and a LabelArgs object -func ObjectWithLabelArgs(clicontext *cli.Context) (string, map[string]string) { +func ObjectWithLabelArgs(cliContext *cli.Context) (string, map[string]string) { var ( - first = clicontext.Args().First() - labelStrings = clicontext.Args().Tail() + first = cliContext.Args().First() + labelStrings = cliContext.Args().Tail() ) return first, LabelArgs(labelStrings) @@ -42,13 +245,10 @@ func ObjectWithLabelArgs(clicontext *cli.Context) (string, map[string]string) { func LabelArgs(labelStrings []string) map[string]string { labels := make(map[string]string, len(labelStrings)) for _, label := range labelStrings { - parts := strings.SplitN(label, "=", 2) - key := parts[0] - value := "true" - if len(parts) > 1 { - value = parts[1] + key, value, ok := strings.Cut(label, "=") + if !ok { + value = "true" } - labels[key] = value } @@ -59,17 +259,17 @@ func LabelArgs(labelStrings []string) map[string]string { func AnnotationArgs(annoStrings []string) (map[string]string, error) { annotations := make(map[string]string, len(annoStrings)) for _, anno := range annoStrings { - parts := strings.SplitN(anno, "=", 2) - if len(parts) != 2 { + key, value, ok := strings.Cut(anno, "=") + if !ok { return nil, fmt.Errorf("invalid key=value format annotation: %v", anno) } - annotations[parts[0]] = parts[1] + annotations[key] = value } return annotations, nil } // PrintAsJSON prints input in JSON format -func PrintAsJSON(x interface{}) { +func PrintAsJSON(x any) { b, err := json.MarshalIndent(x, "", " ") if err != nil { fmt.Fprintf(os.Stderr, "can't marshal %+v as a JSON string: %v\n", x, err) diff --git a/cmd/ctr/commands/containers/checkpoint.go b/cmd/ctr/commands/containers/checkpoint.go index 5bce0bd9..da048f1c 100644 --- a/cmd/ctr/commands/containers/checkpoint.go +++ b/cmd/ctr/commands/containers/checkpoint.go @@ -26,34 +26,34 @@ import ( "github.com/urfave/cli/v2" ) -var checkpointCommand = cli.Command{ +var checkpointCommand = &cli.Command{ Name: "checkpoint", - Usage: "checkpoint a container", + Usage: "Checkpoint a container", ArgsUsage: "CONTAINER REF", Flags: []cli.Flag{ &cli.BoolFlag{ Name: "rw", - Usage: "include the rw layer in the checkpoint", + Usage: "Include the rw layer in the checkpoint", }, &cli.BoolFlag{ Name: "image", - Usage: "include the image in the checkpoint", + Usage: "Include the image in the checkpoint", }, &cli.BoolFlag{ Name: "task", - Usage: "checkpoint container task", + Usage: "Checkpoint container task", }, }, - Action: func(context *cli.Context) error { - id := context.Args().First() + Action: func(cliContext *cli.Context) error { + id := cliContext.Args().First() if id == "" { return errors.New("container id must be provided") } - ref := context.Args().Get(1) + ref := cliContext.Args().Get(1) if ref == "" { return errors.New("ref must be provided") } - client, ctx, cancel, err := commands.NewClient(context) + client, ctx, cancel, err := commands.NewClient(cliContext) if err != nil { return err } @@ -62,13 +62,13 @@ var checkpointCommand = cli.Command{ containerd.WithCheckpointRuntime, } - if context.Bool("image") { + if cliContext.Bool("image") { opts = append(opts, containerd.WithCheckpointImage) } - if context.Bool("rw") { + if cliContext.Bool("rw") { opts = append(opts, containerd.WithCheckpointRW) } - if context.Bool("task") { + if cliContext.Bool("task") { opts = append(opts, containerd.WithCheckpointTask) } container, err := client.LoadContainer(ctx, id) diff --git a/cmd/ctr/commands/containers/containers.go b/cmd/ctr/commands/containers/containers.go index 7b26e317..5895a025 100644 --- a/cmd/ctr/commands/containers/containers.go +++ b/cmd/ctr/commands/containers/containers.go @@ -38,39 +38,39 @@ import ( // Command is the cli command for managing containers var Command = &cli.Command{ Name: "containers", - Usage: "manage containers", + Usage: "Manage containers", Aliases: []string{"c", "container"}, Subcommands: []*cli.Command{ - &createCommand, - &deleteCommand, - &infoCommand, - &listCommand, - &setLabelsCommand, - &checkpointCommand, - &restoreCommand, + createCommand, + deleteCommand, + infoCommand, + listCommand, + setLabelsCommand, + checkpointCommand, + restoreCommand, }, } -var createCommand = cli.Command{ +var createCommand = &cli.Command{ Name: "create", - Usage: "create container", + Usage: "Create container", ArgsUsage: "[flags] Image|RootFS CONTAINER [COMMAND] [ARG...]", - Flags: append(commands.RuntimeFlags, append(append(commands.SnapshotterFlags, commands.ContainerFlags...), flags.ImageDecryptionFlags...)...), - Action: func(context *cli.Context) error { + Flags: append(commands.RuntimeFlags, append(append(append(commands.SnapshotterFlags, []cli.Flag{commands.SnapshotterLabels}...), commands.ContainerFlags...), flags.ImageDecryptionFlags...)...), + Action: func(cliContext *cli.Context) error { var ( id string ref string - config = context.IsSet("config") + config = cliContext.IsSet("config") ) if config { - id = context.Args().First() - if context.NArg() > 1 { + id = cliContext.Args().First() + if cliContext.NArg() > 1 { return fmt.Errorf("with spec config file, only container id should be provided: %w", errdefs.ErrInvalidArgument) } } else { - id = context.Args().Get(1) - ref = context.Args().First() + id = cliContext.Args().Get(1) + ref = cliContext.Args().First() if ref == "" { return fmt.Errorf("image ref must be provided: %w", errdefs.ErrInvalidArgument) } @@ -78,12 +78,12 @@ var createCommand = cli.Command{ if id == "" { return fmt.Errorf("container id must be provided: %w", errdefs.ErrInvalidArgument) } - client, ctx, cancel, err := commands.NewClient(context) + client, ctx, cancel, err := commands.NewClient(cliContext) if err != nil { return err } defer cancel() - _, err = run.NewContainer(ctx, client, context) + _, err = run.NewContainer(ctx, client, cliContext) if err != nil { return err } @@ -91,24 +91,24 @@ var createCommand = cli.Command{ }, } -var listCommand = cli.Command{ +var listCommand = &cli.Command{ Name: "list", Aliases: []string{"ls"}, - Usage: "list containers", + Usage: "List containers", ArgsUsage: "[flags] [, ...]", Flags: []cli.Flag{ &cli.BoolFlag{ Name: "quiet", Aliases: []string{"q"}, - Usage: "print only the container id", + Usage: "Print only the container id", }, }, - Action: func(context *cli.Context) error { + Action: func(cliContext *cli.Context) error { var ( - filters = context.Args().Slice() - quiet = context.Bool("quiet") + filters = cliContext.Args().Slice() + quiet = cliContext.Bool("quiet") ) - client, ctx, cancel, err := commands.NewClient(context) + client, ctx, cancel, err := commands.NewClient(cliContext) if err != nil { return err } @@ -146,33 +146,33 @@ var listCommand = cli.Command{ }, } -var deleteCommand = cli.Command{ +var deleteCommand = &cli.Command{ Name: "delete", - Usage: "delete one or more existing containers", + Usage: "Delete one or more existing containers", ArgsUsage: "[flags] CONTAINER [CONTAINER, ...]", Aliases: []string{"del", "remove", "rm"}, Flags: []cli.Flag{ &cli.BoolFlag{ Name: "keep-snapshot", - Usage: "do not clean up snapshot with container", + Usage: "Do not clean up snapshot with container", }, }, - Action: func(context *cli.Context) error { + Action: func(cliContext *cli.Context) error { var exitErr error - client, ctx, cancel, err := commands.NewClient(context) + client, ctx, cancel, err := commands.NewClient(cliContext) if err != nil { return err } defer cancel() deleteOpts := []containerd.DeleteOpts{} - if !context.Bool("keep-snapshot") { + if !cliContext.Bool("keep-snapshot") { deleteOpts = append(deleteOpts, containerd.WithSnapshotCleanup) } - if context.NArg() == 0 { + if cliContext.NArg() == 0 { return fmt.Errorf("must specify at least one container to delete: %w", errdefs.ErrInvalidArgument) } - for _, arg := range context.Args().Slice() { + for _, arg := range cliContext.Args().Slice() { if err := deleteContainer(ctx, client, arg, deleteOpts...); err != nil { if exitErr == nil { exitErr = err @@ -207,18 +207,18 @@ func deleteContainer(ctx context.Context, client *containerd.Client, id string, } -var setLabelsCommand = cli.Command{ +var setLabelsCommand = &cli.Command{ Name: "label", - Usage: "set and clear labels for a container", + Usage: "Set and clear labels for a container", ArgsUsage: "[flags] CONTAINER [=, ...]", Description: "set and clear labels for a container", Flags: []cli.Flag{}, - Action: func(context *cli.Context) error { - containerID, labels := commands.ObjectWithLabelArgs(context) + Action: func(cliContext *cli.Context) error { + containerID, labels := commands.ObjectWithLabelArgs(cliContext) if containerID == "" { return fmt.Errorf("container id must be provided: %w", errdefs.ErrInvalidArgument) } - client, ctx, cancel, err := commands.NewClient(context) + client, ctx, cancel, err := commands.NewClient(cliContext) if err != nil { return err } @@ -245,22 +245,22 @@ var setLabelsCommand = cli.Command{ }, } -var infoCommand = cli.Command{ +var infoCommand = &cli.Command{ Name: "info", - Usage: "get info about a container", + Usage: "Get info about a container", ArgsUsage: "CONTAINER", Flags: []cli.Flag{ &cli.BoolFlag{ Name: "spec", - Usage: "only display the spec", + Usage: "Only display the spec", }, }, - Action: func(context *cli.Context) error { - id := context.Args().First() + Action: func(cliContext *cli.Context) error { + id := cliContext.Args().First() if id == "" { return fmt.Errorf("container id must be provided: %w", errdefs.ErrInvalidArgument) } - client, ctx, cancel, err := commands.NewClient(context) + client, ctx, cancel, err := commands.NewClient(cliContext) if err != nil { return err } @@ -273,7 +273,7 @@ var infoCommand = cli.Command{ if err != nil { return err } - if context.Bool("spec") { + if cliContext.Bool("spec") { v, err := typeurl.UnmarshalAny(info.Spec) if err != nil { return err @@ -289,7 +289,7 @@ var infoCommand = cli.Command{ } commands.PrintAsJSON(struct { containers.Container - Spec interface{} `json:"Spec,omitempty"` + Spec any `json:"Spec,omitempty"` }{ Container: info, Spec: v, diff --git a/cmd/ctr/commands/containers/restore.go b/cmd/ctr/commands/containers/restore.go index e4a045f5..e63b25b9 100644 --- a/cmd/ctr/commands/containers/restore.go +++ b/cmd/ctr/commands/containers/restore.go @@ -19,37 +19,40 @@ package containers import ( "errors" + "github.com/containerd/console" containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/containerd/v2/cmd/ctr/commands" + "github.com/containerd/containerd/v2/cmd/ctr/commands/tasks" "github.com/containerd/containerd/v2/pkg/cio" "github.com/containerd/errdefs" + "github.com/containerd/log" "github.com/urfave/cli/v2" ) -var restoreCommand = cli.Command{ +var restoreCommand = &cli.Command{ Name: "restore", - Usage: "restore a container from checkpoint", + Usage: "Restore a container from checkpoint", ArgsUsage: "CONTAINER REF", Flags: []cli.Flag{ &cli.BoolFlag{ Name: "rw", - Usage: "restore the rw layer from the checkpoint", + Usage: "Restore the rw layer from the checkpoint", }, &cli.BoolFlag{ Name: "live", - Usage: "restore the runtime and memory data from the checkpoint", + Usage: "Restore the runtime and memory data from the checkpoint", }, }, - Action: func(context *cli.Context) error { - id := context.Args().First() + Action: func(cliContext *cli.Context) error { + id := cliContext.Args().First() if id == "" { return errors.New("container id must be provided") } - ref := context.Args().Get(1) + ref := cliContext.Args().Get(1) if ref == "" { return errors.New("ref must be provided") } - client, ctx, cancel, err := commands.NewClient(context) + client, ctx, cancel, err := commands.NewClient(cliContext) if err != nil { return err } @@ -73,7 +76,7 @@ var restoreCommand = cli.Command{ containerd.WithRestoreSpec, containerd.WithRestoreRuntime, } - if context.Bool("rw") { + if cliContext.Bool("rw") { opts = append(opts, containerd.WithRestoreRW) } @@ -81,17 +84,60 @@ var restoreCommand = cli.Command{ if err != nil { return err } - topts := []containerd.NewTaskOpts{} - if context.Bool("live") { + if cliContext.Bool("live") { topts = append(topts, containerd.WithTaskCheckpoint(checkpoint)) } + spec, err := ctr.Spec(ctx) + if err != nil { + return err + } - task, err := ctr.NewTask(ctx, cio.NewCreator(cio.WithStdio), topts...) + useTTY := spec.Process.Terminal + + var con console.Console + if useTTY { + con = console.Current() + defer con.Reset() + if err := con.SetRaw(); err != nil { + return err + } + } + + task, err := tasks.NewTask(ctx, client, ctr, "", con, false, "", []cio.Opt{}, topts...) if err != nil { return err } - return task.Start(ctx) + var statusC <-chan containerd.ExitStatus + if useTTY { + if statusC, err = task.Wait(ctx); err != nil { + return err + } + } + + if err := task.Start(ctx); err != nil { + return err + } + if !useTTY { + return nil + } + + if err := tasks.HandleConsoleResize(ctx, task, con); err != nil { + log.G(ctx).WithError(err).Error("console resize") + } + + status := <-statusC + code, _, err := status.Result() + if err != nil { + return err + } + if _, err := task.Delete(ctx); err != nil { + return err + } + if code != 0 { + return cli.Exit("", int(code)) + } + return nil }, } diff --git a/cmd/ctr/commands/images/convert.go b/cmd/ctr/commands/images/convert.go index 664bb4c1..28bb8dd6 100644 --- a/cmd/ctr/commands/images/convert.go +++ b/cmd/ctr/commands/images/convert.go @@ -19,22 +19,25 @@ package images import ( "errors" "fmt" + "strings" "github.com/containerd/containerd/v2/cmd/ctr/commands" "github.com/containerd/containerd/v2/core/images/converter" + "github.com/containerd/containerd/v2/core/images/converter/erofs" "github.com/containerd/containerd/v2/core/images/converter/uncompress" "github.com/containerd/platforms" - ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/urfave/cli/v2" ) -var convertCommand = cli.Command{ +var convertCommand = &cli.Command{ Name: "convert", - Usage: "convert an image", + Usage: "Convert an image", ArgsUsage: "[flags] ", Description: `Convert an image format. -e.g., 'ctr convert --uncompress --oci example.com/foo:orig example.com/foo:converted' +e.g., 'ctr image convert --uncompress --oci example.com/foo:orig example.com/foo:converted' + 'ctr image convert --erofs raw example.com/foo:orig example.com/foo:erofs' + 'ctr image convert --erofs zstd example.com/foo:orig example.com/foo:erofs-zstd' Use '--platform' to define the output platform. When '--all-platforms' is given all images in a manifest list must be available. @@ -43,40 +46,49 @@ When '--all-platforms' is given all images in a manifest list must be available. // generic flags &cli.BoolFlag{ Name: "uncompress", - Usage: "convert tar.gz layers to uncompressed tar layers", + Usage: "Convert tar.gz layers to uncompressed tar layers", }, &cli.BoolFlag{ Name: "oci", - Usage: "convert Docker media types to OCI media types", + Usage: "Convert Docker media types to OCI media types", + }, + // erofs flags + &cli.StringFlag{ + Name: "erofs", + Usage: "Convert layers to EROFS format, must specify 'raw' or 'zstd' (e.g. --erofs raw, --erofs zstd)", + }, + &cli.StringFlag{ + Name: "erofs-compressors", + Usage: "Specify compression algorithm list when converting EROFS layers", + }, + &cli.StringFlag{ + Name: "erofs-mkfs-options", + Usage: "Extra mkfs options applied when converting EROFS layers. (e.g. '-Efragments,dedupe')", }, // platform flags &cli.StringSliceFlag{ Name: "platform", Usage: "Pull content from a specific platform", - Value: &cli.StringSlice{}, + Value: cli.NewStringSlice(), }, &cli.BoolFlag{ Name: "all-platforms", - Usage: "exports content from all platforms", + Usage: "Exports content from all platforms", }, }, - Action: func(context *cli.Context) error { + Action: func(cliContext *cli.Context) error { var convertOpts []converter.Opt - srcRef := context.Args().Get(0) - targetRef := context.Args().Get(1) + srcRef := cliContext.Args().Get(0) + targetRef := cliContext.Args().Get(1) if srcRef == "" || targetRef == "" { return errors.New("src and target image need to be specified") } - if !context.Bool("all-platforms") { - if pss := context.StringSlice("platform"); len(pss) > 0 { - var all []ocispec.Platform - for _, ps := range pss { - p, err := platforms.Parse(ps) - if err != nil { - return fmt.Errorf("invalid platform %q: %w", ps, err) - } - all = append(all, p) + if !cliContext.Bool("all-platforms") { + if pss := cliContext.StringSlice("platform"); len(pss) > 0 { + all, err := platforms.ParseAll(pss) + if err != nil { + return err } convertOpts = append(convertOpts, converter.WithPlatform(platforms.Ordered(all...))) } else { @@ -84,15 +96,35 @@ When '--all-platforms' is given all images in a manifest list must be available. } } - if context.Bool("uncompress") { + if cliContext.Bool("uncompress") { convertOpts = append(convertOpts, converter.WithLayerConvertFunc(uncompress.LayerConvertFunc)) } - if context.Bool("oci") { + if cliContext.IsSet("erofs") { + var erofsOpts []erofs.ConvertOpt + switch cliContext.String("erofs") { + case "raw": + case "zstd": + erofsOpts = append(erofsOpts, erofs.WithBlobCompression("zstd")) + default: + return fmt.Errorf("unsupported erofs format %q, supported: raw, zstd", cliContext.String("erofs")) + } + if compressors := cliContext.String("erofs-compressors"); compressors != "" { + erofsOpts = append(erofsOpts, erofs.WithCompressors(compressors)) + } + if mkfsOptsStr := cliContext.String("erofs-mkfs-options"); mkfsOptsStr != "" { + mkfsOpts := strings.Fields(mkfsOptsStr) + erofsOpts = append(erofsOpts, erofs.WithMkfsOptions(mkfsOpts)) + } + convertOpts = append(convertOpts, converter.WithLayerConvertFunc(erofs.LayerConvertFunc(erofsOpts...))) + convertOpts = append(convertOpts, converter.WithUpdateManifest(erofs.UpdateManifestPlatform)) + } + + if cliContext.Bool("oci") { convertOpts = append(convertOpts, converter.WithDockerToOCI(true)) } - client, ctx, cancel, err := commands.NewClient(context) + client, ctx, cancel, err := commands.NewClient(cliContext) if err != nil { return err } @@ -102,7 +134,7 @@ When '--all-platforms' is given all images in a manifest list must be available. if err != nil { return err } - fmt.Fprintln(context.App.Writer, newImg.Target.Digest.String()) + fmt.Fprintln(cliContext.App.Writer, newImg.Target.Digest.String()) return nil }, } diff --git a/cmd/ctr/commands/images/decrypt.go b/cmd/ctr/commands/images/decrypt.go index 203e17db..9c530e8b 100644 --- a/cmd/ctr/commands/images/decrypt.go +++ b/cmd/ctr/commands/images/decrypt.go @@ -30,9 +30,9 @@ import ( "github.com/urfave/cli/v2" ) -var decryptCommand = cli.Command{ +var decryptCommand = &cli.Command{ Name: "decrypt", - Usage: "decrypt an image locally", + Usage: "Decrypt an image locally", ArgsUsage: "[flags] ", Description: `Decrypt an image locally. diff --git a/cmd/ctr/commands/images/encrypt.go b/cmd/ctr/commands/images/encrypt.go index 955e5215..c25a8aff 100644 --- a/cmd/ctr/commands/images/encrypt.go +++ b/cmd/ctr/commands/images/encrypt.go @@ -29,9 +29,9 @@ import ( "github.com/urfave/cli/v2" ) -var encryptCommand = cli.Command{ +var encryptCommand = &cli.Command{ Name: "encrypt", - Usage: "encrypt an image locally", + Usage: "Encrypt an image locally", ArgsUsage: "[flags] ", Description: `Encrypt an image locally. diff --git a/cmd/ctr/commands/images/export.go b/cmd/ctr/commands/images/export.go index 00b7e06a..f271a655 100644 --- a/cmd/ctr/commands/images/export.go +++ b/cmd/ctr/commands/images/export.go @@ -22,16 +22,19 @@ import ( "io" "os" + "github.com/urfave/cli/v2" + "github.com/containerd/containerd/v2/cmd/ctr/commands" "github.com/containerd/containerd/v2/core/images/archive" + "github.com/containerd/containerd/v2/core/transfer" + tarchive "github.com/containerd/containerd/v2/core/transfer/archive" + "github.com/containerd/containerd/v2/core/transfer/image" "github.com/containerd/platforms" - ocispec "github.com/opencontainers/image-spec/specs-go/v1" - "github.com/urfave/cli/v2" ) -var exportCommand = cli.Command{ +var exportCommand = &cli.Command{ Name: "export", - Usage: "export images", + Usage: "Export images", ArgsUsage: "[flags] ...", Description: `Export images to an OCI tar archive. @@ -43,80 +46,118 @@ When '--all-platforms' is given all images in a manifest list must be available. Flags: []cli.Flag{ &cli.BoolFlag{ Name: "skip-manifest-json", - Usage: "do not add Docker compatible manifest.json to archive", + Usage: "Do not add Docker compatible manifest.json to archive", }, &cli.BoolFlag{ Name: "skip-non-distributable", - Usage: "do not add non-distributable blobs such as Windows layers to archive", + Usage: "Do not add non-distributable blobs such as Windows layers to archive", }, &cli.StringSliceFlag{ Name: "platform", Usage: "Pull content from a specific platform", - Value: &cli.StringSlice{}, + Value: cli.NewStringSlice(), }, &cli.BoolFlag{ Name: "all-platforms", - Usage: "exports content from all platforms", + Usage: "Exports content from all platforms", + }, + &cli.BoolFlag{ + Name: "local", + Usage: "Run export locally rather than through transfer API", }, }, - Action: func(context *cli.Context) error { + Action: func(cliContext *cli.Context) error { var ( - out = context.Args().First() - images = context.Args().Tail() + out = cliContext.Args().First() + images = cliContext.Args().Tail() exportOpts = []archive.ExportOpt{} ) if out == "" || len(images) == 0 { return errors.New("please provide both an output filename and an image reference to export") } - if pss := context.StringSlice("platform"); len(pss) > 0 { - var all []ocispec.Platform - for _, ps := range pss { - p, err := platforms.Parse(ps) - if err != nil { - return fmt.Errorf("invalid platform %q: %w", ps, err) + client, ctx, cancel, err := commands.NewClient(cliContext) + if err != nil { + return err + } + defer cancel() + + var w io.WriteCloser + if out == "-" { + w = os.Stdout + } else { + w, err = os.Create(out) + if err != nil { + return err + } + } + defer w.Close() + + if !cliContext.Bool("local") { + pf, done := ProgressHandler(ctx, os.Stdout) + defer done() + + exportOpts := []tarchive.ExportOpt{} + if pss := cliContext.StringSlice("platform"); len(pss) > 0 { + for _, ps := range pss { + p, err := platforms.Parse(ps) + if err != nil { + return fmt.Errorf("invalid platform %q: %w", ps, err) + } + exportOpts = append(exportOpts, tarchive.WithPlatform(p)) } - all = append(all, p) + } + if cliContext.Bool("all-platforms") { + exportOpts = append(exportOpts, tarchive.WithAllPlatforms) + } + + if cliContext.Bool("skip-manifest-json") { + exportOpts = append(exportOpts, tarchive.WithSkipCompatibilityManifest) + } + + if cliContext.Bool("skip-non-distributable") { + exportOpts = append(exportOpts, tarchive.WithSkipNonDistributableBlobs) + } + + storeOpts := make([]image.StoreOpt, len(images)) + for i, img := range images { + storeOpts[i] = image.WithExtraReference(img) + } + + return client.Transfer(ctx, + image.NewStore("", storeOpts...), + tarchive.NewImageExportStream(w, "", exportOpts...), + transfer.WithProgress(pf), + ) + } + + if pss := cliContext.StringSlice("platform"); len(pss) > 0 { + all, err := platforms.ParseAll(pss) + if err != nil { + return err } exportOpts = append(exportOpts, archive.WithPlatform(platforms.Ordered(all...))) } else { exportOpts = append(exportOpts, archive.WithPlatform(platforms.DefaultStrict())) } - if context.Bool("all-platforms") { + if cliContext.Bool("all-platforms") { exportOpts = append(exportOpts, archive.WithAllPlatforms()) } - if context.Bool("skip-manifest-json") { + if cliContext.Bool("skip-manifest-json") { exportOpts = append(exportOpts, archive.WithSkipDockerManifest()) } - if context.Bool("skip-non-distributable") { + if cliContext.Bool("skip-non-distributable") { exportOpts = append(exportOpts, archive.WithSkipNonDistributableBlobs()) } - client, ctx, cancel, err := commands.NewClient(context) - if err != nil { - return err - } - defer cancel() - is := client.ImageService() for _, img := range images { exportOpts = append(exportOpts, archive.WithImage(is, img)) } - var w io.WriteCloser - if out == "-" { - w = os.Stdout - } else { - w, err = os.Create(out) - if err != nil { - return err - } - } - defer w.Close() - return client.Export(ctx, w, exportOpts...) }, } diff --git a/cmd/ctr/commands/images/images.go b/cmd/ctr/commands/images/images.go index 637c9a62..eb299e50 100644 --- a/cmd/ctr/commands/images/images.go +++ b/cmd/ctr/commands/images/images.go @@ -37,45 +37,48 @@ import ( var Command = &cli.Command{ Name: "images", Aliases: []string{"image", "i"}, - Usage: "manage images", + Usage: "Manage images", Subcommands: cli.Commands{ - &checkCommand, - &exportCommand, - &importCommand, - &listCommand, - &mountCommand, - &unmountCommand, - &pullCommand, - &pushCommand, - &removeCommand, - &tagCommand, - &setLabelsCommand, - &convertCommand, - &encryptCommand, - &decryptCommand, - &layerinfoCommand, + checkCommand, + exportCommand, + importCommand, + inspectCommand, + listCommand, + mountCommand, + unmountCommand, + pullCommand, + pushCommand, + pruneCommand, + removeCommand, + tagCommand, + setLabelsCommand, + convertCommand, + usageCommand, + encryptCommand, + decryptCommand, + layerinfoCommand, }, } -var listCommand = cli.Command{ +var listCommand = &cli.Command{ Name: "list", Aliases: []string{"ls"}, - Usage: "list images known to containerd", + Usage: "List images known to containerd", ArgsUsage: "[flags] [, ...]", Description: "list images registered with containerd", Flags: []cli.Flag{ &cli.BoolFlag{ Name: "quiet", Aliases: []string{"q"}, - Usage: "print only the image refs", + Usage: "Print only the image refs", }, }, - Action: func(context *cli.Context) error { + Action: func(cliContext *cli.Context) error { var ( - filters = context.Args().Slice() - quiet = context.Bool("quiet") + filters = cliContext.Args().Slice() + quiet = cliContext.Bool("quiet") ) - client, ctx, cancel, err := commands.NewClient(context) + client, ctx, cancel, err := commands.NewClient(cliContext) if err != nil { return err } @@ -142,24 +145,24 @@ var listCommand = cli.Command{ }, } -var setLabelsCommand = cli.Command{ +var setLabelsCommand = &cli.Command{ Name: "label", - Usage: "set and clear labels for an image", + Usage: "Set and clear labels for an image", ArgsUsage: "[flags] [=, ...]", Description: "set and clear labels for an image", Flags: []cli.Flag{ &cli.BoolFlag{ Name: "replace-all", Aliases: []string{"r"}, - Usage: "replace all labels", + Usage: "Replace all labels", }, }, - Action: func(context *cli.Context) error { + Action: func(cliContext *cli.Context) error { var ( - replaceAll = context.Bool("replace-all") - name, labels = commands.ObjectWithLabelArgs(context) + replaceAll = cliContext.Bool("replace-all") + name, labels = commands.ObjectWithLabelArgs(cliContext) ) - client, ctx, cancel, err := commands.NewClient(context) + client, ctx, cancel, err := commands.NewClient(cliContext) if err != nil { return err } @@ -202,24 +205,24 @@ var setLabelsCommand = cli.Command{ }, } -var checkCommand = cli.Command{ +var checkCommand = &cli.Command{ Name: "check", - Usage: "check existing images to ensure all content is available locally", + Usage: "Check existing images to ensure all content is available locally", ArgsUsage: "[flags] [, ...]", Description: "check existing images to ensure all content is available locally", Flags: append([]cli.Flag{ &cli.BoolFlag{ Name: "quiet", Aliases: []string{"q"}, - Usage: "print only the ready image refs (fully downloaded and unpacked)", + Usage: "Print only the ready image refs (fully downloaded and unpacked)", }, }, commands.SnapshotterFlags...), - Action: func(context *cli.Context) error { + Action: func(cliContext *cli.Context) error { var ( exitErr error - quiet = context.Bool("quiet") + quiet = cliContext.Bool("quiet") ) - client, ctx, cancel, err := commands.NewClient(context) + client, ctx, cancel, err := commands.NewClient(cliContext) if err != nil { return err } @@ -227,7 +230,7 @@ var checkCommand = cli.Command{ var contentStore = client.ContentStore() - args := context.Args().Slice() + args := cliContext.Args().Slice() imageList, err := client.ListImages(ctx, args...) if err != nil { return fmt.Errorf("failed listing images: %w", err) @@ -244,11 +247,11 @@ var checkCommand = cli.Command{ for _, image := range imageList { var ( - status string = "complete" + status = "complete" size string requiredSize int64 presentSize int64 - complete bool = true + complete = true ) available, required, present, missing, err := images.Check(ctx, contentStore, image.Target(), platforms.Default()) @@ -287,7 +290,7 @@ var checkCommand = cli.Command{ size = "-" } - unpacked, err := image.IsUnpacked(ctx, context.String("snapshotter")) + unpacked, err := image.IsUnpacked(ctx, cliContext.String("snapshotter")) if err != nil { if exitErr == nil { exitErr = fmt.Errorf("unable to check unpack for %v: %w", image.Name(), err) @@ -304,7 +307,7 @@ var checkCommand = cli.Command{ size, unpacked) } else { - if complete { + if complete && unpacked { fmt.Println(image.Name()) } } @@ -316,10 +319,10 @@ var checkCommand = cli.Command{ }, } -var removeCommand = cli.Command{ +var removeCommand = &cli.Command{ Name: "delete", Aliases: []string{"del", "remove", "rm"}, - Usage: "remove one or more images by reference", + Usage: "Remove one or more images by reference", ArgsUsage: "[flags] [, ...]", Description: "remove one or more images by reference", Flags: []cli.Flag{ @@ -328,8 +331,8 @@ var removeCommand = cli.Command{ Usage: "Synchronously remove image and all associated resources", }, }, - Action: func(context *cli.Context) error { - client, ctx, cancel, err := commands.NewClient(context) + Action: func(cliContext *cli.Context) error { + client, ctx, cancel, err := commands.NewClient(cliContext) if err != nil { return err } @@ -338,9 +341,9 @@ var removeCommand = cli.Command{ exitErr error imageStore = client.ImageService() ) - for i, target := range context.Args().Slice() { + for i, target := range cliContext.Args().Slice() { var opts []images.DeleteOpt - if context.Bool("sync") && i == context.NArg()-1 { + if cliContext.Bool("sync") && i == cliContext.NArg()-1 { opts = append(opts, images.SynchronousDelete()) } if err := imageStore.Delete(ctx, target, opts...); err != nil { @@ -361,3 +364,73 @@ var removeCommand = cli.Command{ return exitErr }, } + +var pruneCommand = &cli.Command{ + Name: "prune", + Usage: "Remove unused images", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "all", // TODO: add more filters + Usage: "Remove all unused images, not just dangling ones (if all is not specified no images will be pruned)", + }, + }, + // adapted from `nerdctl`: + // https://github.com/containerd/nerdctl/blob/272dc9c29fc1434839d3ec63194d7efa24d7c0ef/cmd/nerdctl/image_prune.go#L86 + Action: func(cliContext *cli.Context) error { + client, ctx, cancel, err := commands.NewClient(cliContext) + if err != nil { + return err + } + defer cancel() + + all := cliContext.Bool("all") + if !all { + log.G(ctx).Warn("No images pruned. `image prune` requires --all to be specified.") + // NOP + return nil + } + + var ( + imageStore = client.ImageService() + containerStore = client.ContainerService() + ) + imageList, err := imageStore.List(ctx) + if err != nil { + return err + } + containerList, err := containerStore.List(ctx) + if err != nil { + return err + } + usedImages := make(map[string]struct{}) + for _, container := range containerList { + usedImages[container.Image] = struct{}{} + } + + var removedImages []string + for _, image := range imageList { + if _, ok := usedImages[image.Name]; ok { + continue + } + removedImages = append(removedImages, image.Name) + } + + var delOpts []images.DeleteOpt + for i, imageName := range removedImages { + // Delete the last image reference synchronously to trigger garbage collection. + // This is best effort. It is possible that the image reference is deleted by + // someone else before this point. + if i == len(removedImages)-1 { + delOpts = []images.DeleteOpt{images.SynchronousDelete()} + } + if err := imageStore.Delete(ctx, imageName, delOpts...); err != nil { + if !errdefs.IsNotFound(err) { + log.G(ctx).WithError(err).Warnf("failed to delete image %s", imageName) + } + continue + } + log.G(ctx).Infof("deleted image: %s\n", imageName) + } + return nil + }, +} diff --git a/cmd/ctr/commands/images/import.go b/cmd/ctr/commands/images/import.go index a8e49431..452c5b6d 100644 --- a/cmd/ctr/commands/images/import.go +++ b/cmd/ctr/commands/images/import.go @@ -17,27 +17,33 @@ package images import ( + "errors" "fmt" "io" "os" "time" + "github.com/urfave/cli/v2" + containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/containerd/v2/cmd/ctr/commands" + "github.com/containerd/containerd/v2/core/diff" "github.com/containerd/containerd/v2/core/images/archive" + "github.com/containerd/containerd/v2/core/transfer" + tarchive "github.com/containerd/containerd/v2/core/transfer/archive" + "github.com/containerd/containerd/v2/core/transfer/image" "github.com/containerd/imgcrypt/cmd/ctr/commands/flags" "github.com/containerd/log" "github.com/containerd/platforms" - "github.com/urfave/cli/v2" "github.com/containerd/imgcrypt/v2" "github.com/containerd/imgcrypt/v2/images/encryption" "github.com/containerd/imgcrypt/v2/images/encryption/parsehelpers" ) -var importCommand = cli.Command{ +var importCommand = &cli.Command{ Name: "import", - Usage: "import images", + Usage: "Import images", ArgsUsage: "[flags] ", Description: `Import images from a tar stream. Implemented formats: @@ -60,50 +66,152 @@ Import of an encrypted image requires the decryption key to be passed. Even thou decrypted it is required that the user proofs to be in possession of one of the decryption keys needed for decrypting the image later on. `, - Flags: append(append([]cli.Flag{ + Flags: append(append(append([]cli.Flag{ &cli.StringFlag{ Name: "base-name", Value: "", - Usage: "base image name for added images, when provided only images with this name prefix are imported", + Usage: "Base image name for added images, when provided only images with this name prefix are imported", }, &cli.BoolFlag{ Name: "digests", - Usage: "whether to create digest images (default: false)", + Usage: "Whether to create digest images (default: false)", }, &cli.BoolFlag{ Name: "skip-digest-for-named", - Usage: "skip applying --digests option to images named in the importing tar (use it in conjunction with --digests)", + Usage: "Skip applying --digests option to images named in the importing tar (use it in conjunction with --digests)", }, &cli.StringFlag{ Name: "index-name", - Usage: "image name to keep index as, by default index is discarded", + Usage: "Image name to keep index as, by default index is discarded", }, &cli.BoolFlag{ Name: "all-platforms", - Usage: "imports content for all platforms, false by default", + Usage: "Imports content for all platforms, false by default", }, &cli.StringFlag{ Name: "platform", - Usage: "imports content for specific platform", + Usage: "Imports content for specific platform", }, &cli.BoolFlag{ Name: "no-unpack", - Usage: "skip unpacking the images, false by default", + Usage: "Skip unpacking the images, cannot be used with --discard-unpacked-layers, false by default", + }, + &cli.BoolFlag{ + Name: "local", + Usage: "Run import locally rather than through transfer API", }, &cli.BoolFlag{ Name: "compress-blobs", - Usage: "compress uncompressed blobs when creating manifest (Docker format only)", + Usage: "Compress uncompressed blobs when creating manifest (Docker format only)", + }, + &cli.BoolFlag{ + Name: "discard-unpacked-layers", + Usage: "Allow the garbage collector to clean layers up from the content store after unpacking, cannot be used with --no-unpack, false by default", }, - }, commands.SnapshotterFlags...), flags.ImageDecryptionFlags...), + &cli.BoolFlag{ + Name: "sync-fs", + Usage: "Synchronize the underlying filesystem containing files when unpack images, false by default", + }, + }, commands.SnapshotterFlags...), commands.LabelFlag), flags.ImageDecryptionFlags...), - Action: func(context *cli.Context) error { + Action: func(cliContext *cli.Context) error { var ( - in = context.Args().First() + in = cliContext.Args().First() opts []containerd.ImportOpt platformMatcher platforms.MatchComparer ) - prefix := context.String("base-name") + client, ctx, cancel, err := commands.NewClient(cliContext) + if err != nil { + return err + } + defer cancel() + + // FIXME: + if false { // !cliContext.Bool("local") { + unsupportedFlags := []string{"discard-unpacked-layers"} + for _, s := range unsupportedFlags { + if cliContext.IsSet(s) { + return fmt.Errorf("\"--%s\" requires \"--local\" flag", s) + } + } + var opts []image.StoreOpt + prefix := cliContext.String("base-name") + var overwrite bool + if prefix == "" { + prefix = fmt.Sprintf("import-%s", time.Now().Format("2006-01-02")) + // Allow overwriting auto-generated prefix with named annotation + overwrite = true + } + + labels := cliContext.StringSlice("label") + if len(labels) > 0 { + opts = append(opts, image.WithImageLabels(commands.LabelArgs(labels))) + } + + if cliContext.Bool("digests") { + opts = append(opts, image.WithDigestRef(prefix, overwrite, !cliContext.Bool("skip-digest-for-named"))) + } else { + opts = append(opts, image.WithNamedPrefix(prefix, overwrite)) + } + + // Even with --all-platforms, only the default platform layers are unpacked, + // for compatibility with --local. + // + // This is still not fully compatible with --local, which only unpacks + // the strict-default platform layers. + platUnpack := platforms.DefaultSpec() + if !cliContext.Bool("all-platforms") { + // If platform specified, use that one, if not use default + if platform := cliContext.String("platform"); platform != "" { + platUnpack, err = platforms.Parse(platform) + if err != nil { + return err + } + } + opts = append(opts, image.WithPlatforms(platUnpack)) + } + + if !cliContext.Bool("no-unpack") { + snapshotter := cliContext.String("snapshotter") + opts = append(opts, image.WithUnpack(platUnpack, snapshotter)) + } + + is := image.NewStore(cliContext.String("index-name"), opts...) + + var iopts []tarchive.ImportOpt + + if cliContext.Bool("compress-blobs") { + iopts = append(iopts, tarchive.WithForceCompression) + } + + var r io.ReadCloser + if in == "-" { + r = os.Stdin + } else { + var err error + r, err = os.Open(in) + if err != nil { + return err + } + } + iis := tarchive.NewImageImportStream(r, "", iopts...) + + pf, done := ProgressHandler(ctx, os.Stdout) + defer done() + + err := client.Transfer(ctx, iis, is, transfer.WithProgress(pf)) + closeErr := r.Close() + if err != nil { + return err + } + + return closeErr + } + + // Local logic + + prefix := cliContext.String("base-name") if prefix == "" { prefix = fmt.Sprintf("import-%s", time.Now().Format("2006-01-02")) opts = append(opts, containerd.WithImageRefTranslator(archive.AddRefPrefix(prefix))) @@ -112,25 +220,25 @@ decrypting the image later on. opts = append(opts, containerd.WithImageRefTranslator(archive.FilterRefPrefix(prefix))) } - if context.Bool("digests") { + if cliContext.Bool("digests") { opts = append(opts, containerd.WithDigestRef(archive.DigestTranslator(prefix))) } - if context.Bool("skip-digest-for-named") { - if !context.Bool("digests") { - return fmt.Errorf("--skip-digest-for-named must be specified with --digests option") + if cliContext.Bool("skip-digest-for-named") { + if !cliContext.Bool("digests") { + return errors.New("--skip-digest-for-named must be specified with --digests option") } opts = append(opts, containerd.WithSkipDigestRef(func(name string) bool { return name != "" })) } - if idxName := context.String("index-name"); idxName != "" { + if idxName := cliContext.String("index-name"); idxName != "" { opts = append(opts, containerd.WithIndexName(idxName)) } - if context.Bool("compress-blobs") { + if cliContext.Bool("compress-blobs") { opts = append(opts, containerd.WithImportCompression()) } - if platform := context.String("platform"); platform != "" { + if platform := cliContext.String("platform"); platform != "" { platSpec, err := platforms.Parse(platform) if err != nil { return err @@ -139,13 +247,25 @@ decrypting the image later on. opts = append(opts, containerd.WithImportPlatform(platformMatcher)) } - opts = append(opts, containerd.WithAllPlatforms(context.Bool("all-platforms"))) + opts = append(opts, containerd.WithAllPlatforms(cliContext.Bool("all-platforms"))) - client, ctx, cancel, err := commands.NewClient(context) + if cliContext.Bool("discard-unpacked-layers") { + if cliContext.Bool("no-unpack") { + return errors.New("--discard-unpacked-layers and --no-unpack are incompatible options") + } + opts = append(opts, containerd.WithDiscardUnpackedLayers()) + } + + labels := cliContext.StringSlice("label") + if len(labels) > 0 { + opts = append(opts, containerd.WithImageLabels(commands.LabelArgs(labels))) + } + + ctx, done, err := client.WithLease(ctx) if err != nil { return err } - defer cancel() + defer done(ctx) var r io.ReadCloser if in == "-" { @@ -156,6 +276,7 @@ decrypting the image later on. return err } } + imgs, err := client.Import(ctx, r, opts...) closeErr := r.Close() if err != nil { @@ -165,8 +286,8 @@ decrypting the image later on. return closeErr } - if !context.Bool("no-unpack") { - cc, err := parsehelpers.CreateDecryptCryptoConfig(ParseEncArgs(context), nil) + if !cliContext.Bool("no-unpack") { + cc, err := parsehelpers.CreateDecryptCryptoConfig(ParseEncArgs(cliContext), nil) if err != nil { return err } @@ -179,13 +300,13 @@ decrypting the image later on. for _, img := range imgs { if platformMatcher == nil { // if platform not specified use default. - platformMatcher = platforms.Default() + platformMatcher = platforms.DefaultStrict() } image := containerd.NewImageWithPlatform(client, img, platformMatcher) // TODO: Show unpack status fmt.Printf("unpacking %s (%s)...", img.Name, img.Target.Digest) - err = image.Unpack(ctx, context.String("snapshotter"), opts) + err = image.Unpack(ctx, cliContext.String("snapshotter"), containerd.WithUnpackApplyOpts(diff.WithSyncFs(cliContext.Bool("sync-fs"))), opts) if err != nil { return err } diff --git a/cmd/ctr/commands/images/inspect.go b/cmd/ctr/commands/images/inspect.go new file mode 100644 index 00000000..42fd154d --- /dev/null +++ b/cmd/ctr/commands/images/inspect.go @@ -0,0 +1,65 @@ +/* + Copyright The containerd 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 images + +import ( + "os" + + "github.com/containerd/containerd/v2/cmd/ctr/commands" + "github.com/containerd/containerd/v2/pkg/display" + "github.com/urfave/cli/v2" +) + +var inspectCommand = &cli.Command{ + Name: "inspect", + Aliases: []string{"i"}, + Usage: "inspect an image", + ArgsUsage: " [flags]", + Description: `Inspect an image`, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "content", + Usage: "Show JSON content", + }, + }, + Action: func(cliContext *cli.Context) error { + client, ctx, cancel, err := commands.NewClient(cliContext) + if err != nil { + return err + } + defer cancel() + var ( + ref = cliContext.Args().First() + imageStore = client.ImageService() + cs = client.ContentStore() + ) + + img, err := imageStore.Get(ctx, ref) + if err != nil { + return err + } + + opts := []display.PrintOpt{ + display.WithWriter(os.Stdout), + } + if cliContext.Bool("content") { + opts = append(opts, display.Verbose) + } + + return display.NewImageTreePrinter(opts...).PrintImageTree(ctx, img, cs) + }, +} diff --git a/cmd/ctr/commands/images/layerinfo.go b/cmd/ctr/commands/images/layerinfo.go index bb1984e0..23ad65be 100644 --- a/cmd/ctr/commands/images/layerinfo.go +++ b/cmd/ctr/commands/images/layerinfo.go @@ -34,9 +34,9 @@ import ( "github.com/urfave/cli/v2" ) -var layerinfoCommand = cli.Command{ +var layerinfoCommand = &cli.Command{ Name: "layerinfo", - Usage: "get information about an image's layers", + Usage: "Get information about an image's layers", ArgsUsage: "[flags] ", Description: `Get encryption information about the layers of an image. diff --git a/cmd/ctr/commands/images/mount.go b/cmd/ctr/commands/images/mount.go index ffd313ba..1c030089 100644 --- a/cmd/ctr/commands/images/mount.go +++ b/cmd/ctr/commands/images/mount.go @@ -17,24 +17,29 @@ package images import ( + "crypto/rand" + "encoding/base64" + "errors" "fmt" "time" + "github.com/containerd/errdefs" + "github.com/containerd/platforms" + "github.com/opencontainers/image-spec/identity" + "github.com/urfave/cli/v2" + containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/containerd/v2/cmd/ctr/commands" + "github.com/containerd/containerd/v2/core/diff" "github.com/containerd/containerd/v2/core/leases" "github.com/containerd/containerd/v2/core/mount" "github.com/containerd/containerd/v2/defaults" - "github.com/containerd/errdefs" - "github.com/containerd/platforms" - "github.com/opencontainers/image-spec/identity" - "github.com/urfave/cli/v2" ) -var mountCommand = cli.Command{ +var mountCommand = &cli.Command{ Name: "mount", - Usage: "mount an image to a target path", - ArgsUsage: "[flags] ", + Usage: "Mount an image to a target path", + ArgsUsage: "[flags] []", Description: `Mount an image rootfs to a specified path. When you are done, use the unmount command. @@ -49,36 +54,50 @@ When you are done, use the unmount command. Usage: "Mount the image for the specified platform", Value: platforms.DefaultString(), }, + &cli.BoolFlag{ + Name: "sync-fs", + Usage: "Synchronize the underlying filesystem containing files when unpack images, false by default", + }, + &cli.DurationFlag{ + Name: "expiration", + Aliases: []string{"x"}, + Usage: "Set the expiration time for the mount and snapshots", + Value: 1 * time.Hour, + }, ), - Action: func(context *cli.Context) (retErr error) { + Action: func(cliContext *cli.Context) (retErr error) { var ( - ref = context.Args().First() - target = context.Args().Get(1) + ref = cliContext.Args().First() + target = cliContext.Args().Get(1) ) if ref == "" { - return fmt.Errorf("please provide an image reference to mount") + return errors.New("please provide an image reference to mount") } + + var key string if target == "" { - return fmt.Errorf("please provide a target path to mount to") + t := time.Now() + var b [3]byte + rand.Read(b[:]) + key = fmt.Sprintf("ctr-images-mount-%d-%s", t.Nanosecond(), base64.URLEncoding.EncodeToString(b[:])) + } else { + key = target } - client, ctx, cancel, err := commands.NewClient(context) + client, ctx, cancel, err := commands.NewClient(cliContext) if err != nil { return err } defer cancel() - snapshotter := context.String("snapshotter") + snapshotter := cliContext.String("snapshotter") if snapshotter == "" { snapshotter = defaults.DefaultSnapshotter } ctx, done, err := client.WithLease(ctx, - leases.WithID(target), - leases.WithExpiration(24*time.Hour), - leases.WithLabels(map[string]string{ - "containerd.io/gc.ref.snapshot." + snapshotter: target, - }), + leases.WithID(key), + leases.WithExpiration(cliContext.Duration("expiration")), ) if err != nil && !errdefs.IsAlreadyExists(err) { return err @@ -90,7 +109,7 @@ When you are done, use the unmount command. } }() - ps := context.String("platform") + ps := cliContext.String("platform") p, err := platforms.Parse(ps) if err != nil { return fmt.Errorf("unable to parse platform %s: %w", ps, err) @@ -102,7 +121,7 @@ When you are done, use the unmount command. } i := containerd.NewImageWithPlatform(client, img, platforms.Only(p)) - if err := i.Unpack(ctx, snapshotter); err != nil { + if err := i.Unpack(ctx, snapshotter, containerd.WithUnpackApplyOpts(diff.WithSyncFs(cliContext.Bool("sync-fs")))); err != nil { return fmt.Errorf("error unpacking image: %w", err) } @@ -116,28 +135,43 @@ When you are done, use the unmount command. s := client.SnapshotService(snapshotter) var mounts []mount.Mount - if context.Bool("rw") { - mounts, err = s.Prepare(ctx, target, chainID) + if cliContext.Bool("rw") { + mounts, err = s.Prepare(ctx, key, chainID) } else { - mounts, err = s.View(ctx, target, chainID) + mounts, err = s.View(ctx, key, chainID) } if err != nil { if errdefs.IsAlreadyExists(err) { - mounts, err = s.Mounts(ctx, target) + mounts, err = s.Mounts(ctx, key) } if err != nil { return err } } - if err := mount.All(mounts, target); err != nil { - if err := s.Remove(ctx, target); err != nil && !errdefs.IsNotFound(err) { - fmt.Fprintln(context.App.ErrWriter, "Error cleaning up snapshot after mount error:", err) + mm := client.MountManager() + + info, err := mm.Activate(ctx, key, mounts, mount.WithTemporary) + if err == nil { + mounts = info.System + } else if !errdefs.IsNotImplemented(err) { + return fmt.Errorf("activate error: %w", err) + } + + if target != "" { + if err := mount.All(mounts, target); err != nil { + if err := s.Remove(ctx, key); err != nil && !errdefs.IsNotFound(err) { + fmt.Fprintln(cliContext.App.ErrWriter, "Error cleaning up snapshot after mount error:", err) + } + return fmt.Errorf("failed to mount %v: %w", mounts, err) } - return err + } else if len(mounts) == 1 && mounts[0].Type == "bind" { + target = mounts[0].Source + } else { + return fmt.Errorf("cannot handle returned mounts: %v", mounts) } - fmt.Fprintln(context.App.Writer, target) + fmt.Fprintln(cliContext.App.Writer, target) return nil }, } diff --git a/cmd/ctr/commands/images/pull.go b/cmd/ctr/commands/images/pull.go index 1ce923d3..00039bdb 100644 --- a/cmd/ctr/commands/images/pull.go +++ b/cmd/ctr/commands/images/pull.go @@ -17,13 +17,23 @@ package images import ( + "context" + "errors" "fmt" + "io" + "os" + "strings" "time" containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/containerd/v2/cmd/ctr/commands" "github.com/containerd/containerd/v2/cmd/ctr/commands/content" + "github.com/containerd/containerd/v2/core/diff" "github.com/containerd/containerd/v2/core/images" + "github.com/containerd/containerd/v2/core/transfer" + "github.com/containerd/containerd/v2/core/transfer/image" + "github.com/containerd/containerd/v2/core/transfer/registry" + "github.com/containerd/containerd/v2/pkg/progress" "github.com/containerd/imgcrypt/cmd/ctr/commands/flags" "github.com/containerd/log" "github.com/containerd/platforms" @@ -37,9 +47,9 @@ import ( "github.com/urfave/cli/v2" ) -var pullCommand = cli.Command{ +var pullCommand = &cli.Command{ Name: "pull", - Usage: "pull an image from a remote", + Usage: "Pull an image from a remote", ArgsUsage: "[flags] ", Description: `Fetch and prepare an image for use in containerd. @@ -54,15 +64,20 @@ command. As part of this process, we do the following: &cli.StringSliceFlag{ Name: "platform", Usage: "Pull content from a specific platform", - Value: &cli.StringSlice{}, + Value: cli.NewStringSlice(), }, &cli.BoolFlag{ Name: "all-platforms", - Usage: "pull content and metadata from all platforms", + Usage: "Pull content and metadata from all platforms", }, &cli.BoolFlag{ - Name: "all-metadata", - Usage: "Pull metadata for all platforms", + Name: "all-metadata", + Usage: "(Deprecated: use skip-metadata) Pull metadata for all platforms", + Hidden: true, + }, + &cli.BoolFlag{ + Name: "skip-metadata", + Usage: "Skips metadata for unused platforms (Image may be unable to be pushed without metadata)", }, &cli.BoolFlag{ Name: "print-chainid", @@ -72,29 +87,117 @@ command. As part of this process, we do the following: Name: "max-concurrent-downloads", Usage: "Set the max concurrent downloads for each pull", }, + &cli.BoolFlag{ + Name: "local", + Usage: "Fetch content from local client rather than using transfer service", + }, + &cli.BoolFlag{ + Name: "sync-fs", + Usage: "Synchronize the underlying filesystem containing files when unpack images, false by default", + }, ), flags.ImageDecryptionFlags..., ), - Action: func(context *cli.Context) error { + Action: func(cliContext *cli.Context) error { var ( - ref = context.Args().First() + ref = cliContext.Args().First() ) if ref == "" { - return fmt.Errorf("please provide an image reference to pull") + return errors.New("please provide an image reference to pull") } - client, ctx, cancel, err := commands.NewClient(context) + client, ctx, cancel, err := commands.NewClient(cliContext) if err != nil { return err } defer cancel() + if !cliContext.Bool("local") { + unsupportedFlags := []string{"max-concurrent-downloads", "print-chainid", + "skip-verify", "tlscacert", "tlscert", "tlskey", // RegistryFlags + } + for _, s := range unsupportedFlags { + if cliContext.IsSet(s) { + return fmt.Errorf("\"--%s\" requires \"--local\" flag", s) + } + } + + ch, err := commands.NewStaticCredentials(ctx, cliContext, ref) + if err != nil { + return err + } + + var sopts []image.StoreOpt + p, err := platforms.ParseAll(cliContext.StringSlice("platform")) + if err != nil { + return err + } + allPlatforms := cliContext.Bool("all-platforms") + if len(p) > 0 && allPlatforms { + return errors.New("cannot specify both --platform and --all-platforms") + } + if len(p) == 0 && !allPlatforms { + spec := platforms.DefaultSpec() + // Use linux by default for unpacking images on darwin as configured by transfer service + if spec.OS == "darwin" { + spec.OS = "linux" + } + p = append(p, spec) + } + // we use an empty `Platform` slice to indicate that we want to pull all platforms + sopts = append(sopts, image.WithPlatforms(p...)) + // TODO: Support unpack for all platforms..? + // Pass in a *? + for _, platform := range p { + sopts = append(sopts, image.WithUnpack(platform, cliContext.String("snapshotter"))) + } + + if cliContext.Bool("metadata-only") { + sopts = append(sopts, image.WithAllMetadata) + // Any with an empty set is None + // TODO: Specify way to specify not default platform + // config.PlatformMatcher = platforms.Any() + } else if !cliContext.Bool("skip-metadata") { + sopts = append(sopts, image.WithAllMetadata) + } + labels := cliContext.StringSlice("label") + if len(labels) > 0 { + sopts = append(sopts, image.WithImageLabels(commands.LabelArgs(labels))) + } + + opts := []registry.Opt{ + registry.WithCredentials(ch), + registry.WithHostDir(cliContext.String("hosts-dir")), + } + if cliContext.Bool("plain-http") { + opts = append(opts, registry.WithDefaultScheme("http")) + } + logStream := log.G(ctx).Writer() + if cliContext.Bool("http-dump") { + opts = append(opts, registry.WithHTTPDebug(), registry.WithClientStream(logStream)) + } + if cliContext.Bool("http-trace") { + opts = append(opts, registry.WithHTTPTrace(), registry.WithClientStream(logStream)) + } + reg, err := registry.NewOCIRegistry(ctx, ref, opts...) + if err != nil { + return err + } + is := image.NewStore(ref, sopts...) + + pf, done := ProgressHandler(ctx, os.Stdout) + defer done() + + return client.Transfer(ctx, reg, is, transfer.WithProgress(pf)) + } + ctx, done, err := client.WithLease(ctx) if err != nil { return err } defer done(ctx) - config, err := content.NewFetchConfig(ctx, context) + // TODO: Handle this locally via transfer config + config, err := content.NewFetchConfig(ctx, cliContext) if err != nil { return err } @@ -109,25 +212,22 @@ command. As part of this process, we do the following: // TODO: Show unpack status var p []ocispec.Platform - if context.Bool("all-platforms") { + if cliContext.Bool("all-platforms") { p, err = images.Platforms(ctx, client.ContentStore(), img.Target) if err != nil { return fmt.Errorf("unable to resolve image platforms: %w", err) } } else { - for _, s := range context.StringSlice("platform") { - ps, err := platforms.Parse(s) - if err != nil { - return fmt.Errorf("unable to parse platform %s: %w", s, err) - } - p = append(p, ps) + p, err = platforms.ParseAll(cliContext.StringSlice("platform")) + if err != nil { + return err } } if len(p) == 0 { p = append(p, platforms.DefaultSpec()) } - cc, err := parsehelpers.CreateDecryptCryptoConfig(ParseEncArgs(context), nil) + cc, err := parsehelpers.CreateDecryptCryptoConfig(ParseEncArgs(cliContext), nil) if err != nil { return err } @@ -140,11 +240,11 @@ command. As part of this process, we do the following: for _, platform := range p { fmt.Printf("unpacking %s %s...\n", platforms.Format(platform), img.Target.Digest) i := containerd.NewImageWithPlatform(client, img, platforms.Only(platform)) - err = i.Unpack(ctx, context.String("snapshotter"), opts) + err = i.Unpack(ctx, cliContext.String("snapshotter"), containerd.WithUnpackApplyOpts(diff.WithSyncFs(cliContext.Bool("sync-fs"))), opts) if err != nil { return err } - if context.Bool("print-chainid") { + if cliContext.Bool("print-chainid") { diffIDs, err := i.RootFS(ctx) if err != nil { return err @@ -157,3 +257,287 @@ command. As part of this process, we do the following: return nil }, } + +type progressNode struct { + transfer.Progress + children []*progressNode + root bool +} + +func (n *progressNode) mainDesc() *ocispec.Descriptor { + if n.Desc != nil { + return n.Desc + } + for _, c := range n.children { + if desc := c.mainDesc(); desc != nil { + return desc + } + } + return nil +} + +// ProgressHandler continuously updates the output with job progress +// by checking status in the content store. +func ProgressHandler(ctx context.Context, out io.Writer) (transfer.ProgressFunc, func()) { + ctx, cancel := context.WithCancel(ctx) + var ( + fw = progress.NewWriter(out) + start = time.Now() + statuses = map[string]*progressNode{} + roots = []*progressNode{} + progress transfer.ProgressFunc + // Use a buffered channel for progress to allow multiple completed + // progress updates to be processed before shutting down the progress + // handler. Currently the progress stream does not have an explicit + // end, however, done indicates the server has already completed + // sending all progress. + pc = make(chan transfer.Progress, 5) + status string + closeC = make(chan struct{}) + ) + + progress = func(p transfer.Progress) { + select { + case pc <- p: + case <-ctx.Done(): + } + } + + done := func() { + cancel() + <-closeC + } + go func() { + defer close(closeC) + for { + select { + case p := <-pc: + if p.Name == "" { + status = p.Event + continue + } + 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 + } + + /* + all := make([]transfer.Progress, 0, len(statuses)) + for _, p := range statuses { + all = append(all, p.Progress) + } + sort.Slice(all, func(i, j int) bool { + return all[i].Name < all[j].Name + }) + Display(fw, status, all, start) + */ + DisplayHierarchy(fw, status, roots, start) + fw.Flush() + case <-ctx.Done(): + return + } + } + }() + + return progress, done +} + +func DisplayHierarchy(w io.Writer, status string, roots []*progressNode, start time.Time) { + total := displayNode(w, "", roots) + for _, r := range roots { + if desc := r.mainDesc(); desc != nil { + fmt.Fprintf(w, "%s %s\n", desc.MediaType, desc.Digest) + } + } + // Print the Status line + fmt.Fprintf(w, "%s\telapsed: %-4.1fs\ttotal: %7.6v\t(%v)\t\n", + status, + time.Since(start).Seconds(), + // TODO(stevvooe): These calculations are actually way off. + // Need to account for previously downloaded data. These + // will basically be right for a download the first time + // but will be skewed if restarting, as it includes the + // data into the start time before. + progress.Bytes(total), + progress.NewBytesPerSecond(total, time.Since(start))) +} + +func displayNode(w io.Writer, prefix string, nodes []*progressNode) int64 { + var total int64 + for i, node := range nodes { + status := node.Progress + total += status.Progress + pf, cpf := prefixes(i, len(nodes)) + if node.root { + pf, cpf = "", "" + } + + name := prefix + pf + displayName(status.Name) + + switch status.Event { + case "downloading", "uploading", "extracting": + var bar progress.Bar + if status.Total > 0.0 { + bar = progress.Bar(float64(status.Progress) / float64(status.Total)) + } + fmt.Fprintf(w, "%-40.40s\t%-11s\t%40r\t%8.8s/%s\t\n", + name, + status.Event, + bar, + progress.Bytes(status.Progress), progress.Bytes(status.Total)) + case "resolving", "waiting": + bar := progress.Bar(0.0) + fmt.Fprintf(w, "%-40.40s\t%-11s\t%40r\t\n", + name, + status.Event, + bar) + case "complete", "extracted": + bar := progress.Bar(1.0) + fmt.Fprintf(w, "%-40.40s\t%-11s\t%40r\t\n", + name, + status.Event, + bar) + default: + fmt.Fprintf(w, "%-40.40s\t%s\t\n", + name, + status.Event) + } + total += displayNode(w, prefix+cpf, node.children) + } + return total +} + +func prefixes(index, length int) (prefix string, childPrefix string) { + if index+1 == length { + prefix = "└──" + childPrefix = " " + } else { + prefix = "├──" + childPrefix = "│ " + } + return +} + +func displayName(name string) string { + parts := strings.Split(name, "-") + for i := range parts { + parts[i] = shortenName(parts[i]) + } + return strings.Join(parts, " ") +} + +func shortenName(name string) string { + if strings.HasPrefix(name, "sha256:") && len(name) == 71 { + return "(" + name[7:19] + ")" + } + return name +} + +// Display pretty prints out the download or upload progress +// Status tree +func Display(w io.Writer, status string, statuses []transfer.Progress, start time.Time) { + var total int64 + for _, status := range statuses { + total += status.Progress + switch status.Event { + case "downloading", "uploading": + var bar progress.Bar + if status.Total > 0.0 { + bar = progress.Bar(float64(status.Progress) / float64(status.Total)) + } + fmt.Fprintf(w, "%s:\t%s\t%40r\t%8.8s/%s\t\n", + status.Name, + status.Event, + bar, + progress.Bytes(status.Progress), progress.Bytes(status.Total)) + case "resolving", "waiting": + bar := progress.Bar(0.0) + fmt.Fprintf(w, "%s:\t%s\t%40r\t\n", + status.Name, + status.Event, + bar) + case "complete", "done": + bar := progress.Bar(1.0) + fmt.Fprintf(w, "%s:\t%s\t%40r\t\n", + status.Name, + status.Event, + bar) + default: + fmt.Fprintf(w, "%s:\t%s\t\n", + status.Name, + status.Event) + } + } + + // Print the Status line + fmt.Fprintf(w, "%s\telapsed: %-4.1fs\ttotal: %7.6v\t(%v)\t\n", + status, + time.Since(start).Seconds(), + // TODO(stevvooe): These calculations are actually way off. + // Need to account for previously downloaded data. These + // will basically be right for a download the first time + // but will be skewed if restarting, as it includes the + // data into the start time before. + progress.Bytes(total), + progress.NewBytesPerSecond(total, time.Since(start))) +} diff --git a/cmd/ctr/commands/images/push.go b/cmd/ctr/commands/images/push.go index 3c3d2a2a..c9d1ebcd 100644 --- a/cmd/ctr/commands/images/push.go +++ b/cmd/ctr/commands/images/push.go @@ -17,10 +17,9 @@ package images import ( - gocontext "context" + "context" "errors" "fmt" - "net/http/httptrace" "os" "sync" "text/tabwriter" @@ -32,6 +31,10 @@ import ( "github.com/containerd/containerd/v2/core/images" "github.com/containerd/containerd/v2/core/remotes" "github.com/containerd/containerd/v2/core/remotes/docker" + "github.com/containerd/containerd/v2/core/transfer" + "github.com/containerd/containerd/v2/core/transfer/image" + "github.com/containerd/containerd/v2/core/transfer/registry" + "github.com/containerd/containerd/v2/pkg/httpdbg" "github.com/containerd/containerd/v2/pkg/progress" "github.com/containerd/log" "github.com/containerd/platforms" @@ -41,9 +44,9 @@ import ( "golang.org/x/sync/errgroup" ) -var pushCommand = cli.Command{ +var pushCommand = &cli.Command{ Name: "push", - Usage: "push an image to a remote", + Usage: "Push an image to a remote", ArgsUsage: "[flags] []", Description: `Pushes an image reference from containerd. @@ -56,45 +59,90 @@ var pushCommand = cli.Command{ `, Flags: append(commands.RegistryFlags, &cli.StringFlag{ Name: "manifest", - Usage: "digest of manifest", + Usage: "Digest of manifest", }, &cli.StringFlag{ Name: "manifest-type", - Usage: "media type of manifest digest", + Usage: "Media type of manifest digest", Value: ocispec.MediaTypeImageManifest, }, &cli.StringSliceFlag{ Name: "platform", - Usage: "push content from a specific platform", - Value: &cli.StringSlice{}, + Usage: "Push content from a specific platform", + Value: cli.NewStringSlice(), }, &cli.IntFlag{ Name: "max-concurrent-uploaded-layers", Usage: "Set the max concurrent uploaded layers for each push", + }, &cli.BoolFlag{ + Name: "local", + Usage: "Push content from local client rather than using transfer service", }, &cli.BoolFlag{ Name: "allow-non-distributable-blobs", Usage: "Allow pushing blobs that are marked as non-distributable", }), - Action: func(context *cli.Context) error { + Action: func(cliContext *cli.Context) error { var ( - ref = context.Args().First() - local = context.Args().Get(1) - debug = context.Bool("debug") + ref = cliContext.Args().First() + local = cliContext.Args().Get(1) + debug = cliContext.Bool("debug") desc ocispec.Descriptor ) if ref == "" { return errors.New("please provide a remote image reference to push") } - client, ctx, cancel, err := commands.NewClient(context) + client, ctx, cancel, err := commands.NewClient(cliContext) if err != nil { return err } defer cancel() - if manifest := context.String("manifest"); manifest != "" { + if !cliContext.Bool("local") { + unsupportedFlags := []string{ + "manifest", "manifest-type", "max-concurrent-uploaded-layers", "allow-non-distributable-blobs", + "skip-verify", "tlscacert", "tlscert", "tlskey", "http-dump", "http-trace", // RegistryFlags + } + for _, s := range unsupportedFlags { + if cliContext.IsSet(s) { + return fmt.Errorf("\"--%s\" requires \"--local\" flag", s) + } + } + + ch, err := commands.NewStaticCredentials(ctx, cliContext, ref) + if err != nil { + return err + } + + if local == "" { + local = ref + } + opts := []registry.Opt{registry.WithCredentials(ch), registry.WithHostDir(cliContext.String("hosts-dir"))} + if cliContext.Bool("plain-http") { + opts = append(opts, registry.WithDefaultScheme("http")) + } + reg, err := registry.NewOCIRegistry(ctx, ref, opts...) + if err != nil { + return err + } + var p []ocispec.Platform + if pss := cliContext.StringSlice("platform"); len(pss) > 0 { + p, err = platforms.ParseAll(pss) + if err != nil { + return fmt.Errorf("invalid platform %v: %w", pss, err) + } + } + is := image.NewStore(local, image.WithPlatforms(p...)) + + pf, done := ProgressHandler(ctx, os.Stdout) + defer done() + + return client.Transfer(ctx, is, reg, transfer.WithProgress(pf)) + } + + if manifest := cliContext.String("manifest"); manifest != "" { desc.Digest, err = digest.Parse(manifest) if err != nil { return fmt.Errorf("invalid manifest digest: %w", err) } - desc.MediaType = context.String("manifest-type") + desc.MediaType = cliContext.String("manifest-type") } else { if local == "" { local = ref @@ -105,7 +153,7 @@ var pushCommand = cli.Command{ } desc = img.Target - if pss := context.StringSlice("platform"); len(pss) == 1 { + if pss := cliContext.StringSlice("platform"); len(pss) == 1 { p, err := platforms.Parse(pss[0]) if err != nil { return fmt.Errorf("invalid platform %q: %w", pss[0], err) @@ -127,10 +175,10 @@ var pushCommand = cli.Command{ } } - if context.Bool("http-trace") { - ctx = httptrace.WithClientTrace(ctx, commands.NewDebugClientTrace(ctx)) + if cliContext.Bool("http-trace") { + ctx = httpdbg.WithClientTrace(ctx) } - resolver, err := commands.GetResolver(ctx, context) + resolver, err := commands.GetResolver(ctx, cliContext) if err != nil { return err } @@ -146,8 +194,8 @@ var pushCommand = cli.Command{ log.G(ctx).WithField("image", ref).WithField("digest", desc.Digest).Debug("pushing") - jobHandler := images.HandlerFunc(func(ctx gocontext.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { - if !context.Bool("allow-non-distributable-blobs") && images.IsNonDistributable(desc.MediaType) { + jobHandler := images.HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { + if !cliContext.Bool("allow-non-distributable-blobs") && images.IsNonDistributable(desc.MediaType) { return nil, nil } ongoing.add(remotes.MakeRefKey(ctx, desc)) @@ -155,7 +203,7 @@ var pushCommand = cli.Command{ }) handler := jobHandler - if !context.Bool("allow-non-distributable-blobs") { + if !cliContext.Bool("allow-non-distributable-blobs") { handler = remotes.SkipNonDistributableBlobs(handler) } @@ -164,8 +212,8 @@ var pushCommand = cli.Command{ containerd.WithImageHandler(handler), } - if context.IsSet("max-concurrent-uploaded-layers") { - mcu := context.Int("max-concurrent-uploaded-layers") + if cliContext.IsSet("max-concurrent-uploaded-layers") { + mcu := cliContext.Int("max-concurrent-uploaded-layers") ropts = append(ropts, containerd.WithMaxConcurrentUploadedLayers(mcu)) } @@ -247,7 +295,7 @@ func (j *pushjobs) status() []content.StatusInfo { status, err := j.tracker.GetStatus(name) if err != nil { - si.Status = "waiting" + si.Status = content.StatusWaiting } else { si.Offset = status.Offset si.Total = status.Total @@ -255,12 +303,12 @@ func (j *pushjobs) status() []content.StatusInfo { si.UpdatedAt = status.UpdatedAt if status.Offset >= status.Total { if status.UploadUUID == "" { - si.Status = "done" + si.Status = content.StatusDone } else { - si.Status = "committing" + si.Status = content.StatusCommitting } } else { - si.Status = "uploading" + si.Status = content.StatusUploading } } statuses = append(statuses, si) diff --git a/cmd/ctr/commands/images/tag.go b/cmd/ctr/commands/images/tag.go index e9abdf50..94fa24ba 100644 --- a/cmd/ctr/commands/images/tag.go +++ b/cmd/ctr/commands/images/tag.go @@ -17,41 +17,69 @@ package images import ( + "errors" "fmt" + "github.com/urfave/cli/v2" + "github.com/containerd/containerd/v2/cmd/ctr/commands" + "github.com/containerd/containerd/v2/core/transfer/image" "github.com/containerd/errdefs" - "github.com/urfave/cli/v2" + "github.com/distribution/reference" ) -var tagCommand = cli.Command{ +var tagCommand = &cli.Command{ Name: "tag", - Usage: "tag an image", + Usage: "Tag an image", ArgsUsage: "[flags] [, ...]", Description: `Tag an image for use in containerd.`, Flags: []cli.Flag{ &cli.BoolFlag{ Name: "force", - Usage: "force target_ref to be created, regardless if it already exists", + Usage: "Force target_ref to be created, regardless if it already exists", + }, + &cli.BoolFlag{ + Name: "local", + Usage: "Run tag locally rather than through transfer API", + }, + &cli.BoolFlag{ + Name: "skip-reference-check", + Usage: "Skip the strict check for reference names", }, }, - Action: func(context *cli.Context) error { + Action: func(cliContext *cli.Context) error { var ( - ref = context.Args().First() + ref = cliContext.Args().First() ) if ref == "" { - return fmt.Errorf("please provide an image reference to tag from") + return errors.New("please provide an image reference to tag from") } - if context.NArg() <= 1 { - return fmt.Errorf("please provide an image reference to tag to") + if cliContext.NArg() <= 1 { + return errors.New("please provide an image reference to tag to") } - client, ctx, cancel, err := commands.NewClient(context) + client, ctx, cancel, err := commands.NewClient(cliContext) if err != nil { return err } defer cancel() + if !cliContext.Bool("local") { + for _, targetRef := range cliContext.Args().Slice()[1:] { + if !cliContext.Bool("skip-reference-check") { + if _, err := reference.ParseAnyReference(targetRef); err != nil { + return fmt.Errorf("error parsing reference: %q is not a valid repository/tag %v", targetRef, err) + } + } + err = client.Transfer(ctx, image.NewStore(ref), image.NewStore(targetRef)) + if err != nil { + return err + } + fmt.Println(targetRef) + } + return nil + } + ctx, done, err := client.WithLease(ctx) if err != nil { return err @@ -64,13 +92,18 @@ var tagCommand = cli.Command{ return err } // Support multiple references for one command run - for _, targetRef := range context.Args().Slice()[1:] { + for _, targetRef := range cliContext.Args().Slice()[1:] { + if !cliContext.Bool("skip-reference-check") { + if _, err := reference.ParseAnyReference(targetRef); err != nil { + return fmt.Errorf("error parsing reference: %q is not a valid repository/tag %v", targetRef, err) + } + } image.Name = targetRef // Attempt to create the image first if _, err = imageService.Create(ctx, image); err != nil { // If user has specified force and the image already exists then // delete the original image and attempt to create the new one - if errdefs.IsAlreadyExists(err) && context.Bool("force") { + if errdefs.IsAlreadyExists(err) && cliContext.Bool("force") { if err = imageService.Delete(ctx, targetRef); err != nil { return err } diff --git a/cmd/ctr/commands/images/unmount.go b/cmd/ctr/commands/images/unmount.go index 9881d082..f3ff8a34 100644 --- a/cmd/ctr/commands/images/unmount.go +++ b/cmd/ctr/commands/images/unmount.go @@ -17,6 +17,7 @@ package images import ( + "errors" "fmt" "github.com/containerd/containerd/v2/cmd/ctr/commands" @@ -26,26 +27,26 @@ import ( "github.com/urfave/cli/v2" ) -var unmountCommand = cli.Command{ +var unmountCommand = &cli.Command{ Name: "unmount", - Usage: "unmount the image from the target", + Usage: "Unmount the image from the target", ArgsUsage: "[flags] ", Description: "Unmount the image rootfs from the specified target.", Flags: append(append(commands.RegistryFlags, append(commands.SnapshotterFlags, commands.LabelFlag)...), &cli.BoolFlag{ Name: "rm", - Usage: "remove the snapshot after a successful unmount", + Usage: "Remove the snapshot after a successful unmount", }, ), - Action: func(context *cli.Context) error { + Action: func(cliContext *cli.Context) error { var ( - target = context.Args().First() + target = cliContext.Args().First() ) if target == "" { - return fmt.Errorf("please provide a target path to unmount from") + return errors.New("please provide a target path to unmount from") } - client, ctx, cancel, err := commands.NewClient(context) + client, ctx, cancel, err := commands.NewClient(cliContext) if err != nil { return err } @@ -55,8 +56,8 @@ var unmountCommand = cli.Command{ return err } - if context.Bool("rm") { - snapshotter := context.String("snapshotter") + if cliContext.Bool("rm") { + snapshotter := cliContext.String("snapshotter") s := client.SnapshotService(snapshotter) if err := client.LeasesService().Delete(ctx, leases.Lease{ID: target}); err != nil && !errdefs.IsNotFound(err) { return fmt.Errorf("error deleting lease: %w", err) @@ -66,7 +67,7 @@ var unmountCommand = cli.Command{ } } - fmt.Fprintln(context.App.Writer, target) + fmt.Fprintln(cliContext.App.Writer, target) return nil }, } diff --git a/cmd/ctr/commands/images/usage.go b/cmd/ctr/commands/images/usage.go new file mode 100644 index 00000000..3f1f44f0 --- /dev/null +++ b/cmd/ctr/commands/images/usage.go @@ -0,0 +1,99 @@ +/* + Copyright The containerd 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 images + +import ( + "errors" + "fmt" + "os" + "text/tabwriter" + + containerd "github.com/containerd/containerd/v2/client" + "github.com/containerd/containerd/v2/cmd/ctr/commands" + "github.com/containerd/containerd/v2/defaults" + "github.com/containerd/containerd/v2/pkg/progress" + + "github.com/opencontainers/image-spec/identity" + "github.com/urfave/cli/v2" +) + +var usageCommand = &cli.Command{ + Name: "usage", + Usage: "Display usage of snapshots for a given image ref", + ArgsUsage: "[flags] ", + Flags: commands.SnapshotterFlags, + Action: func(cliContext *cli.Context) error { + var ref = cliContext.Args().First() + if ref == "" { + return errors.New("please provide an image reference to mount") + } + + client, ctx, cancel, err := commands.NewClient(cliContext) + if err != nil { + return err + } + defer cancel() + + snapshotter := cliContext.String("snapshotter") + if snapshotter == "" { + snapshotter = defaults.DefaultSnapshotter + } + + img, err := client.ImageService().Get(ctx, ref) + if err != nil { + return fmt.Errorf("failed to ensure if image %s exists: %w", ref, err) + } + + i := containerd.NewImage(client, img) + if ok, err := i.IsUnpacked(ctx, snapshotter); err != nil { + return fmt.Errorf("failed to ensure if image %s has been unpacked in snapshotter %s: %w", + ref, snapshotter, err) + } else if !ok { + return fmt.Errorf("image %s isn't unpacked in snapshotter %s", ref, snapshotter) + } + + diffIDs, err := i.RootFS(ctx) + if err != nil { + return err + } + + tw := tabwriter.NewWriter(os.Stdout, 1, 8, 1, ' ', 0) + fmt.Fprintln(tw, "KEY\tSIZE\tINODES\t") + + snSrv := client.SnapshotService(snapshotter) + snID := identity.ChainID(diffIDs).String() + for snID != "" { + usage, err := snSrv.Usage(ctx, snID) + if err != nil { + return fmt.Errorf("failed to get usage for snapshot %s: %w", snID, err) + } + + fmt.Fprintf(tw, "%v\t%s\t%d\t\n", + snID, + progress.Bytes(usage.Size).String(), + usage.Inodes, + ) + + info, err := snSrv.Stat(ctx, snID) + if err != nil { + return fmt.Errorf("failed to ensure if snapshot %s has parent or not: %w", snID, err) + } + snID = info.Parent + } + return tw.Flush() + }, +} diff --git a/cmd/ctr/commands/run/run.go b/cmd/ctr/commands/run/run.go index 09b14269..49f3df2f 100644 --- a/cmd/ctr/commands/run/run.go +++ b/cmd/ctr/commands/run/run.go @@ -18,39 +18,45 @@ package run import ( "context" - gocontext "context" "encoding/csv" + "encoding/json" "errors" "fmt" + "maps" + "os" "strings" "github.com/containerd/console" + gocni "github.com/containerd/go-cni" + specs "github.com/opencontainers/runtime-spec/specs-go" + "github.com/urfave/cli/v2" + + "github.com/containerd/errdefs" + "github.com/containerd/log" + containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/containerd/v2/cmd/ctr/commands" "github.com/containerd/containerd/v2/cmd/ctr/commands/tasks" "github.com/containerd/containerd/v2/core/containers" "github.com/containerd/containerd/v2/pkg/cio" clabels "github.com/containerd/containerd/v2/pkg/labels" - "github.com/containerd/containerd/v2/pkg/namespaces" "github.com/containerd/containerd/v2/pkg/oci" - gocni "github.com/containerd/go-cni" "github.com/containerd/imgcrypt/cmd/ctr/commands/flags" - specs "github.com/opencontainers/runtime-spec/specs-go" - "github.com/sirupsen/logrus" - "github.com/urfave/cli/v2" ) -func withMounts(context *cli.Context) oci.SpecOpts { - return func(ctx gocontext.Context, client oci.Client, container *containers.Container, s *specs.Spec) error { +func withMounts(cliContext *cli.Context) oci.SpecOpts { + return func(ctx context.Context, client oci.Client, container *containers.Container, s *specs.Spec) error { mounts := make([]specs.Mount, 0) - for _, mount := range context.StringSlice("mount") { + dests := make([]string, 0) + for _, mount := range cliContext.StringSlice("mount") { m, err := parseMountFlag(mount) if err != nil { return err } mounts = append(mounts, m) + dests = append(dests, m.Destination) } - return oci.WithMounts(mounts)(ctx, client, container, s) + return oci.Compose(oci.WithoutMounts(dests...), oci.WithMounts(mounts))(ctx, client, container, s) } } @@ -65,13 +71,11 @@ func parseMountFlag(m string) (specs.Mount, error) { } for _, field := range fields { - v := strings.SplitN(field, "=", 2) - if len(v) < 2 { - return mount, fmt.Errorf("invalid mount specification: expected key=val") + key, val, ok := strings.Cut(field, "=") + if !ok { + return mount, errors.New("invalid mount specification: expected key=val") } - key := v[0] - val := v[1] switch key { case "type": mount.Type = val @@ -92,67 +96,75 @@ func parseMountFlag(m string) (specs.Mount, error) { // Command runs a container var Command = &cli.Command{ Name: "run", - Usage: "run a container", + Usage: "Run a container", ArgsUsage: "[flags] Image|RootFS ID [COMMAND] [ARG...]", Flags: append([]cli.Flag{ &cli.BoolFlag{ Name: "rm", - Usage: "remove the container after running, cannot be used with --detach", + Usage: "Remove the container after running, cannot be used with --detach", }, &cli.BoolFlag{ Name: "null-io", - Usage: "send all IO to /dev/null", + Usage: "Send all IO to /dev/null", }, &cli.StringFlag{ Name: "log-uri", - Usage: "log uri", + Usage: "Log uri", }, &cli.BoolFlag{ Name: "detach", Aliases: []string{"d"}, - Usage: "detach from the task after it has started execution, cannot be used with --rm", + Usage: "Detach from the task after it has started execution, cannot be used with --rm", }, &cli.StringFlag{ Name: "fifo-dir", - Usage: "directory used for storing IO FIFOs", + Usage: "Directory used for storing IO FIFOs", }, &cli.StringFlag{ Name: "cgroup", - Usage: "cgroup path (To disable use of cgroup, set to \"\" explicitly)", + Usage: "Cgroup path (To disable use of cgroup, set to \"\" explicitly)", }, &cli.StringFlag{ Name: "platform", - Usage: "run image for specific platform", + Usage: "Run image for specific platform", }, &cli.BoolFlag{ Name: "cni", - Usage: "enable cni networking for the container", + Usage: "Enable cni networking for the container", + }, + &cli.BoolFlag{ + Name: "sync-fs", + Usage: "Synchronize the underlying filesystem containing files when unpack images, false by default", + }, + &cli.StringFlag{ + Name: "dump-config", + Usage: "Dump the generated OCI config to a file", }, }, append(platformRunFlags, append(commands.RuntimeFlags, append(append(append(commands.SnapshotterFlags, []cli.Flag{commands.SnapshotterLabels}...), commands.ContainerFlags...), flags.ImageDecryptionFlags...)...)...)...), - Action: func(context *cli.Context) error { + Action: func(cliContext *cli.Context) error { var ( err error id string ref string - rm = context.Bool("rm") - tty = context.Bool("tty") - detach = context.Bool("detach") - config = context.IsSet("config") - enableCNI = context.Bool("cni") + rm = cliContext.Bool("rm") + tty = cliContext.Bool("tty") + detach = cliContext.Bool("detach") + config = cliContext.IsSet("config") + enableCNI = cliContext.Bool("cni") ) if config { - id = context.Args().First() - if context.NArg() > 1 { + id = cliContext.Args().First() + if cliContext.NArg() > 1 { return errors.New("with spec config file, only container id should be provided") } } else { - id = context.Args().Get(1) - ref = context.Args().First() + id = cliContext.Args().Get(1) + ref = cliContext.Args().First() if ref == "" { return errors.New("image ref must be provided") @@ -165,17 +177,39 @@ var Command = &cli.Command{ return errors.New("flags --detach and --rm cannot be specified together") } - client, ctx, cancel, err := commands.NewClient(context) + client, ctx, cancel, err := commands.NewClient(cliContext) if err != nil { return err } defer cancel() - container, err := NewContainer(ctx, client, context) + + container, err := NewContainer(ctx, client, cliContext) if err != nil { return err } if rm && !detach { - defer container.Delete(ctx, containerd.WithSnapshotCleanup) + defer func() { + if err := container.Delete(ctx, containerd.WithSnapshotCleanup); err != nil { + log.L.WithError(err).Error("failed to cleanup container") + } + }() + } + if cliContext.IsSet("dump-config") { + filename := cliContext.String("dump-config") + if filename == "" { + return errors.New("file name is required with --dump-config") + } + spec, err := container.Spec(ctx) + if err != nil { + return err + } + specBytes, err := json.MarshalIndent(spec, "", " ") + if err != nil { + return err + } + if err := os.WriteFile(filename, specBytes, 0o666); err != nil { + return err + } } var con console.Console if tty { @@ -192,9 +226,9 @@ var Command = &cli.Command{ } } - opts := getNewTaskOpts(context) - ioOpts := []cio.Opt{cio.WithFIFODir(context.String("fifo-dir"))} - task, err := tasks.NewTask(ctx, client, container, context.String("checkpoint"), con, context.Bool("null-io"), context.String("log-uri"), ioOpts, opts...) + opts := tasks.GetNewTaskOpts(cliContext) + ioOpts := []cio.Opt{cio.WithFIFODir(cliContext.String("fifo-dir"))} + task, err := tasks.NewTask(ctx, client, container, cliContext.String("checkpoint"), con, cliContext.Bool("null-io"), cliContext.String("log-uri"), ioOpts, opts...) if err != nil { return err } @@ -203,19 +237,22 @@ var Command = &cli.Command{ if !detach { defer func() { if enableCNI { - if err := network.Remove(ctx, fullID(ctx, container), ""); err != nil { - logrus.WithError(err).Error("network review") + if err := network.Remove(ctx, commands.FullID(ctx, container), ""); err != nil { + log.L.WithError(err).Error("failed to remove network") } } - task.Delete(ctx) + + if _, err := task.Delete(ctx, containerd.WithProcessKill); err != nil && !errdefs.IsNotFound(err) { + log.L.WithError(err).Error("failed to cleanup task") + } }() if statusC, err = task.Wait(ctx); err != nil { return err } } - if context.IsSet("pid-file") { - if err := commands.WritePidFile(context.String("pid-file"), int(task.Pid())); err != nil { + if cliContext.IsSet("pid-file") { + if err := commands.WritePidFile(cliContext.String("pid-file"), int(task.Pid())); err != nil { return err } } @@ -225,7 +262,7 @@ var Command = &cli.Command{ return err } - if _, err := network.Setup(ctx, fullID(ctx, container), netNsPath); err != nil { + if _, err := network.Setup(ctx, commands.FullID(ctx, container), netNsPath); err != nil { return err } } @@ -237,7 +274,7 @@ var Command = &cli.Command{ } if tty { if err := tasks.HandleConsoleResize(ctx, task, con); err != nil { - logrus.WithError(err).Error("console resize") + log.L.WithError(err).Error("console resize") } } else { sigc := commands.ForwardAllSignals(ctx, task) @@ -252,22 +289,13 @@ var Command = &cli.Command{ return err } if code != 0 { - return cli.NewExitError("", int(code)) + return cli.Exit("", int(code)) } return nil }, } -func fullID(ctx context.Context, c containerd.Container) string { - id := c.ID() - ns, ok := namespaces.Namespace(ctx) - if !ok { - return id - } - return fmt.Sprintf("%s-%s", ns, id) -} - -// buildLabel builds the labels from command line labels and the image labels +// buildLabels builds the labels from command line labels and the image labels func buildLabels(cmdLabels, imageLabels map[string]string) map[string]string { labels := make(map[string]string) for k, v := range imageLabels { @@ -276,12 +304,10 @@ func buildLabels(cmdLabels, imageLabels map[string]string) map[string]string { } else { // In case the image label is invalid, we output a warning and skip adding it to the // container. - logrus.WithError(err).Warnf("unable to add image label with key %s to the container", k) + log.L.WithError(err).Warnf("unable to add image label with key %s to the container", k) } } // labels from the command line will override image and the initial image config labels - for k, v := range cmdLabels { - labels[k] = v - } + maps.Copy(labels, cmdLabels) return labels } diff --git a/cmd/ctr/commands/run/run_unix.go b/cmd/ctr/commands/run/run_unix.go index adc996f1..2b6fb99b 100644 --- a/cmd/ctr/commands/run/run_unix.go +++ b/cmd/ctr/commands/run/run_unix.go @@ -32,7 +32,7 @@ import ( runtimeoptions "github.com/containerd/containerd/api/types/runtimeoptions/v1" containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/containerd/v2/contrib/apparmor" - "github.com/containerd/containerd/v2/contrib/nvidia" + //"github.com/containerd/containerd/v2/contrib/nvidia" "github.com/containerd/containerd/v2/contrib/seccomp" "github.com/containerd/containerd/v2/core/containers" "github.com/containerd/containerd/v2/core/snapshots" @@ -303,9 +303,9 @@ func NewContainer(ctx gocontext.Context, client *containerd.Client, context *cli Path: parts[1], })) } - if context.IsSet("gpus") { - opts = append(opts, nvidia.WithGPUs(nvidia.WithDevices(context.IntSlice("gpus")...), nvidia.WithAllCapabilities)) - } + //if context.IsSet("gpus") { + // opts = append(opts, nvidia.WithGPUs(nvidia.WithDevices(context.IntSlice("gpus")...), nvidia.WithAllCapabilities)) + //} if context.IsSet("allow-new-privs") { opts = append(opts, oci.WithNewPrivileges) } diff --git a/cmd/ctr/main.go b/cmd/ctr/main.go index dbb1527e..c209da8e 100644 --- a/cmd/ctr/main.go +++ b/cmd/ctr/main.go @@ -27,10 +27,10 @@ import ( var pluginCmds = []*cli.Command{} func main() { - application := app.New() - application.Commands = append(application.Commands, pluginCmds...) - if err := application.Run(os.Args); err != nil { - _, _ = fmt.Fprintf(os.Stderr, "ctr: %s\n", err) + app := app.New() + app.Commands = append(app.Commands, pluginCmds...) + if err := app.Run(os.Args); err != nil { + fmt.Fprintf(os.Stderr, "ctr: %s\n", err) os.Exit(1) } } diff --git a/cmd/go.mod b/cmd/go.mod index 8c925ec7..e7d0a3c8 100644 --- a/cmd/go.mod +++ b/cmd/go.mod @@ -1,13 +1,13 @@ module github.com/containerd/imgcrypt/cmd -go 1.25.0 +go 1.26.3 require ( - github.com/Microsoft/go-winio v0.6.2 - github.com/Microsoft/hcsshim v0.12.9 + github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 + github.com/Microsoft/hcsshim v0.15.0-rc.1 github.com/containerd/console v1.0.5 - github.com/containerd/containerd/api v1.10.0 - github.com/containerd/containerd/v2 v2.0.10 + github.com/containerd/containerd/api v1.11.1 + github.com/containerd/containerd/v2 v2.3.2 github.com/containerd/errdefs v1.0.0 github.com/containerd/go-cni v1.1.13 github.com/containerd/imgcrypt/v2 v2.0.2 @@ -15,11 +15,12 @@ require ( github.com/containerd/platforms v1.0.0-rc.4 github.com/containerd/typeurl/v2 v2.2.3 github.com/containers/ocicrypt v1.3.1 + github.com/distribution/reference v0.6.0 github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.1.1 - github.com/opencontainers/runtime-spec v1.2.0 + github.com/opencontainers/runtime-spec v1.3.0 github.com/sirupsen/logrus v1.9.4 - github.com/urfave/cli/v2 v2.27.5 + github.com/urfave/cli/v2 v2.27.7 golang.org/x/sync v0.21.0 google.golang.org/grpc v1.82.0 ) @@ -29,20 +30,19 @@ require ( github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20231105174938-2b5cbb29f3e2 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cilium/ebpf v0.11.0 // indirect - github.com/containerd/cgroups/v3 v3.0.3 // indirect - github.com/containerd/continuity v0.4.4 // indirect + github.com/cilium/ebpf v0.16.0 // indirect + github.com/containerd/cgroups/v3 v3.1.3 // indirect + github.com/containerd/continuity v0.5.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/fifo v1.1.0 // indirect github.com/containerd/go-runc v1.1.0 // indirect - github.com/containerd/plugin v1.0.0 // indirect - github.com/containerd/ttrpc v1.2.7 // indirect + github.com/containerd/plugin v1.1.0 // indirect + github.com/containerd/ttrpc v1.2.8 // indirect github.com/containernetworking/cni v1.3.0 // indirect - github.com/containernetworking/plugins v1.5.1 // indirect - github.com/coreos/go-systemd/v22 v22.5.0 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect + github.com/containernetworking/plugins v1.9.1 // indirect + github.com/coreos/go-systemd/v22 v22.7.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/cyphar/filepath-securejoin v0.6.0 // indirect - github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-jose/go-jose/v4 v4.1.4 // indirect @@ -50,10 +50,11 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/google/go-cmp v0.7.0 // indirect - github.com/klauspost/compress v1.17.11 // indirect - github.com/mdlayher/socket v0.4.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/compress v1.18.5 // indirect + github.com/mdlayher/socket v0.5.1 // indirect github.com/mdlayher/vsock v1.2.1 // indirect github.com/miekg/pkcs11 v1.1.2 // indirect github.com/moby/locker v1.0.1 // indirect @@ -61,10 +62,10 @@ require ( github.com/moby/sys/sequential v0.6.0 // indirect github.com/moby/sys/signal v0.7.1 // indirect github.com/moby/sys/symlink v0.3.0 // indirect - github.com/moby/sys/user v0.3.0 // indirect + github.com/moby/sys/user v0.4.0 // indirect github.com/moby/sys/userns v0.1.0 // indirect github.com/opencontainers/selinux v1.13.1 // indirect - github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pelletier/go-toml/v2 v2.3.0 // indirect github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect @@ -74,7 +75,7 @@ require ( github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect go.opentelemetry.io/otel v1.43.0 // indirect go.opentelemetry.io/otel/metric v1.43.0 // indirect go.opentelemetry.io/otel/trace v1.43.0 // indirect @@ -86,7 +87,7 @@ require ( golang.org/x/term v0.44.0 // indirect golang.org/x/text v0.38.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect - google.golang.org/protobuf v1.36.11 // indirect + google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect ) replace github.com/containerd/imgcrypt/v2 => ../ diff --git a/cmd/go.sum b/cmd/go.sum index 6136010a..584f89d6 100644 --- a/cmd/go.sum +++ b/cmd/go.sum @@ -8,25 +8,39 @@ github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20231105174938-2b5cbb29f3e2/go.mod github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 h1:0kQAzHq8vLs7Pptv+7TxjdETLf/nIqJpIB4oC6Ba4vY= +github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29/go.mod h1:ZWa7ssZJT30CCDGJ7fk/2SBTq9BIQrrVjrcss0UW2s0= github.com/Microsoft/hcsshim v0.12.9 h1:2zJy5KA+l0loz1HzEGqyNnjd3fyZA31ZBCGKacp6lLg= github.com/Microsoft/hcsshim v0.12.9/go.mod h1:fJ0gkFAna6ukt0bLdKB8djt4XIJhF/vEPuoIWYVvZ8Y= +github.com/Microsoft/hcsshim v0.15.0-rc.1 h1:FbbwtQmiD+BVHynGkx5S65JkLyhkEiiTP8nrpmg2SZw= +github.com/Microsoft/hcsshim v0.15.0-rc.1/go.mod h1:HWvvUPIy9HF6LotILj1G4VyS065rcLQ6tqj6tMUdOfI= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cilium/ebpf v0.11.0 h1:V8gS/bTCCjX9uUnkUFUpPsksM8n1lXBAvHcpiFk1X2Y= github.com/cilium/ebpf v0.11.0/go.mod h1:WE7CZAnqOL2RouJ4f1uyNhqr2P4CCvXFIqdRDUgWsVs= +github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok= +github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/containerd/cgroups/v3 v3.0.3 h1:S5ByHZ/h9PMe5IOQoN7E+nMc2UcLEM/V48DGDJ9kip0= github.com/containerd/cgroups/v3 v3.0.3/go.mod h1:8HBe7V3aWGLFPd/k03swSIsGjZhHI2WzJmticMgVuz0= +github.com/containerd/cgroups/v3 v3.1.3 h1:eUNflyMddm18+yrDmZPn3jI7C5hJ9ahABE5q6dyLYXQ= +github.com/containerd/cgroups/v3 v3.1.3/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw= github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/containerd/containerd/api v1.10.0 h1:5n0oHYVBwN4VhoX9fFykCV9dF1/BvAXeg2F8W6UYq1o= github.com/containerd/containerd/api v1.10.0/go.mod h1:NBm1OAk8ZL+LG8R0ceObGxT5hbUYj7CzTmR3xh0DlMM= +github.com/containerd/containerd/api v1.11.1 h1:h8nfoDW9+fNsC/9TwiAHj8B1GzXKtR4eFtkhi/X5RLU= +github.com/containerd/containerd/api v1.11.1/go.mod h1:CaQFRu+N1MtbgL6JDOJLUB1hCKESU1lD6MuTJhgtdlw= github.com/containerd/containerd/v2 v2.0.10 h1:l2ueLbt1HN1/WRl2pQvHM+7eEodjknAsXv1HLJ2Mz0Q= github.com/containerd/containerd/v2 v2.0.10/go.mod h1:YdMdboz+mhlo+CQYGaLyUuqJBGlaz2OV2SA6dEMjvuo= +github.com/containerd/containerd/v2 v2.3.2 h1:eLven1YxRMkeiKu7IcMrPKE+gn8sGR1DqHbbshMEvWM= +github.com/containerd/containerd/v2 v2.3.2/go.mod h1:rHKGm3VW6wNrINb3x8mNT+w7qYXFVElTt/8HTuxVhD4= github.com/containerd/continuity v0.4.4 h1:/fNVfTJ7wIl/YPMHjf+5H32uFhl63JucB34PlCpMKII= github.com/containerd/continuity v0.4.4/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= +github.com/containerd/continuity v0.5.0 h1:7a85HZpCSs+1Zps0Ee3DPSuAWY+0SJM1JNM51nlEVDg= +github.com/containerd/continuity v0.5.0/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= @@ -43,25 +57,36 @@ github.com/containerd/platforms v1.0.0-rc.4 h1:M42JrUT4zfZTqtkUwkr0GzmUWbfyO5VO0 github.com/containerd/platforms v1.0.0-rc.4/go.mod h1:lKlMXyLybmBedS/JJm11uDofzI8L2v0J2ZbYvNsbq1A= github.com/containerd/plugin v1.0.0 h1:c8Kf1TNl6+e2TtMHZt+39yAPDbouRH9WAToRjex483Y= github.com/containerd/plugin v1.0.0/go.mod h1:hQfJe5nmWfImiqT1q8Si3jLv3ynMUIBB47bQ+KexvO8= +github.com/containerd/plugin v1.1.0 h1:O+7lczNJVMy8rz0YNx3xGB8tTf5qY4i5abF041Ew19U= +github.com/containerd/plugin v1.1.0/go.mod h1:qBTum+A8lJ6lO44A19Eo7y1OlcLj4OWFH1DA/vnHmcc= github.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRqQ= github.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o= +github.com/containerd/ttrpc v1.2.8 h1:xbVu6D4qF2jihdh9rDVOKqUMiFBQk6YctTdo1zk087Y= +github.com/containerd/ttrpc v1.2.8/go.mod h1:wyZW2K79t4Hfcxl+GUvkZqRBzJlqFFvgEeeWXa42tyE= github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40= github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk= github.com/containernetworking/cni v1.3.0 h1:v6EpN8RznAZj9765HhXQrtXgX+ECGebEYEmnuFjskwo= github.com/containernetworking/cni v1.3.0/go.mod h1:Bs8glZjjFfGPHMw6hQu82RUgEPNGEaBb9KS5KtNMnJ4= github.com/containernetworking/plugins v1.5.1 h1:T5ji+LPYjjgW0QM+KyrigZbLsZ8jaX+E5J/EcKOE4gQ= github.com/containernetworking/plugins v1.5.1/go.mod h1:MIQfgMayGuHYs0XdNudf31cLLAC+i242hNm6KuDGqCM= +github.com/containernetworking/plugins v1.9.1 h1:8oU6WsIsU3bpnNZuvHp74a6cE1MJwbj2P7s4/yTUNlA= +github.com/containernetworking/plugins v1.9.1/go.mod h1:fj7kS55qg3o/RgS+WGsF3+ZxwIImMPusQZKzBpcSr4c= github.com/containers/ocicrypt v1.3.1 h1:s7KzBxIjvqL1YANzONHoUqCVuhurSI6xMC/lWu+Az60= github.com/containers/ocicrypt v1.3.1/go.mod h1:x9k7a/qzgnVeKJbwDKFLeL2bQgsR6uQxz/D+NgF38A0= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= +github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= +github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cyphar/filepath-securejoin v0.6.0 h1:BtGB77njd6SVO6VztOHfPxKitJvd/VPT+OFBFMOi1Is= github.com/cyphar/filepath-securejoin v0.6.0/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= @@ -92,6 +117,8 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -115,6 +142,7 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k= github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= +github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -122,12 +150,16 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= +github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= +github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= github.com/mdlayher/vsock v1.2.1 h1:pC1mTJTvjo1r9n9fbm7S1j04rCgCzhCOS5DY0zqHlnQ= github.com/mdlayher/vsock v1.2.1/go.mod h1:NRfCibel++DgeMD8z/hP+PPTjlNJsdPOmxcnENvE+SE= github.com/miekg/pkcs11 v1.1.2 h1:/VxmeAX5qU6Q3EwafypogwWbYryHFmF2RpkJmw3m4MQ= @@ -144,31 +176,41 @@ github.com/moby/sys/symlink v0.3.0 h1:GZX89mEZ9u53f97npBy4Rc3vJKj7JBDj/PN2I22GrN github.com/moby/sys/symlink v0.3.0/go.mod h1:3eNdhduHmYPcgsJtZXW1W4XUJdZGBIkttZ8xKqPUJq0= github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/onsi/ginkgo/v2 v2.20.1 h1:YlVIbqct+ZmnEph770q9Q7NVAz4wwIiVNahee6JyUzo= github.com/onsi/ginkgo/v2 v2.20.1/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI= +github.com/onsi/ginkgo/v2 v2.25.1 h1:Fwp6crTREKM+oA6Cz4MsO8RhKQzs2/gOIVOUscMAfZY= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +github.com/onsi/gomega v1.38.1 h1:FaLA8GlcpXDwsb7m0h2A9ew2aTk3vnZMlzFgg5tz/pk= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk= github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.3.0 h1:YZupQUdctfhpZy3TM39nN9Ika5CBWT5diQ8ibYCRkxg= +github.com/opencontainers/runtime-spec v1.3.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/selinux v1.13.1 h1:A8nNeceYngH9Ow++M+VVEwJVpdFmrlxsN22F+ISDCJE= github.com/opencontainers/selinux v1.13.1/go.mod h1:S10WXZ/osk2kWOYKy1x2f/eXF5ZHJoUs8UU/2caNRbg= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM= +github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 h1:Dx7Ovyv/SFnMFw3fD4oEoeorXc6saIiQ23LrGLth0Gw= github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= @@ -192,8 +234,11 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= +github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -205,6 +250,8 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= @@ -358,6 +405,8 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=