diff --git a/.cirrus.yml b/.cirrus.yml index b13f75fa6c8..9382a59ad57 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -38,4 +38,4 @@ windows_task: - powershell hack/configure-windows-ci.ps1 - refreshenv - go install .\cmd\nerdctl\ - - go test -v ./cmd/... + - go test -v ./integration/... diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a7ec7913aeb..deffe3f672f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -156,4 +156,4 @@ jobs: run: | sudo apt-get install -y expect - name: "Ensure that the integration test suite is compatible with Docker" - run: go test -timeout 20m -v -exec sudo ./cmd/nerdctl/... -args -test.target=docker -test.kill-daemon + run: go test -timeout 20m -v -exec sudo ./integration/... -args -test.target=docker -test.kill-daemon diff --git a/.gitignore b/.gitignore index 33c9733d860..5b7ac4a4682 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ build # vagrant /.vagrant +!cmd/nerdctl/build diff --git a/Dockerfile b/Dockerfile index f2612545596..8cd5575b4a3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -296,7 +296,7 @@ RUN curl -L -o nydus-static.tgz "https://github.com/dragonflyoss/image-service/r tar xzf nydus-static.tgz && \ mv nydus-static/nydus-image nydus-static/nydusd nydus-static/nydusify /usr/bin/ && \ rm nydus-static.tgz -CMD ["gotestsum", "--format=testname", "--rerun-fails=2", "--packages=github.com/containerd/nerdctl/cmd/nerdctl/...", \ +CMD ["gotestsum", "--format=testname", "--rerun-fails=2", "--packages=github.com/containerd/nerdctl/integration/...", \ "--", "-timeout=20m", "-args", "-test.kill-daemon"] FROM test-integration AS test-integration-rootless @@ -318,11 +318,11 @@ COPY ./Dockerfile.d/etc_systemd_system_user@.service.d_delegate.conf /etc/system # ipfs daemon for rootless containerd will be enabled in /test-integration-rootless.sh RUN systemctl disable test-integration-ipfs-offline VOLUME /home/rootless/.local/share -RUN go test -o /usr/local/bin/nerdctl.test -c ./cmd/nerdctl +RUN go test -o /usr/local/bin/nerdctl.test -c ./integration COPY ./Dockerfile.d/test-integration-rootless.sh / CMD ["/test-integration-rootless.sh", \ "gotestsum", "--format=testname", "--rerun-fails=2", "--raw-command", \ - "--", "/usr/local/go/bin/go", "tool", "test2json", "-t", "-p", "github.com/containerd/nerdctl/cmd/nerdctl", \ + "--", "/usr/local/go/bin/go", "tool", "test2json", "-t", "-p", "github.com/containerd/nerdctl/integration/...", \ "/usr/local/bin/nerdctl.test", "-test.v", "-test.timeout=20m", "-test.kill-daemon"] # test for CONTAINERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=slirp4netns diff --git a/cmd/nerdctl/completion_linux.go b/cmd/nerdctl/apparmor/apparmor_freebsd.go similarity index 56% rename from cmd/nerdctl/completion_linux.go rename to cmd/nerdctl/apparmor/apparmor_freebsd.go index e89d5e40960..e369b44d0e8 100644 --- a/cmd/nerdctl/completion_linux.go +++ b/cmd/nerdctl/apparmor/apparmor_freebsd.go @@ -14,21 +14,10 @@ limitations under the License. */ -package main +package apparmor -import ( - "github.com/containerd/nerdctl/pkg/apparmorutil" - "github.com/spf13/cobra" -) +import "github.com/spf13/cobra" -func shellCompleteApparmorProfiles(cmd *cobra.Command) ([]string, cobra.ShellCompDirective) { - profiles, err := apparmorutil.Profiles() - if err != nil { - return nil, cobra.ShellCompDirectiveError - } - var names []string // nolint: prealloc - for _, f := range profiles { - names = append(names, f.Name) - } - return names, cobra.ShellCompDirectiveNoFileComp +func AddApparmorCommand(rootCmd *cobra.Command) { + // NOP } diff --git a/cmd/nerdctl/apparmor_inspect_linux.go b/cmd/nerdctl/apparmor/apparmor_inspect_linux.go similarity index 91% rename from cmd/nerdctl/apparmor_inspect_linux.go rename to cmd/nerdctl/apparmor/apparmor_inspect_linux.go index a47f812a64c..d6235a9025e 100644 --- a/cmd/nerdctl/apparmor_inspect_linux.go +++ b/cmd/nerdctl/apparmor/apparmor_inspect_linux.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package apparmor import ( "fmt" @@ -29,14 +29,14 @@ func newApparmorInspectCommand() *cobra.Command { Use: "inspect", Short: fmt.Sprintf("Display the default AppArmor profile %q. Other profiles cannot be displayed with this command.", defaults.AppArmorProfileName), Args: cobra.NoArgs, - RunE: apparmorInspectAction, + RunE: InspectAction, SilenceUsage: true, SilenceErrors: true, } return cmd } -func apparmorInspectAction(cmd *cobra.Command, args []string) error { +func InspectAction(cmd *cobra.Command, args []string) error { b, err := apparmor.DumpDefaultProfile(defaults.AppArmorProfileName) if err != nil { return err diff --git a/cmd/nerdctl/apparmor_linux.go b/cmd/nerdctl/apparmor/apparmor_linux.go similarity index 70% rename from cmd/nerdctl/apparmor_linux.go rename to cmd/nerdctl/apparmor/apparmor_linux.go index e39985e9294..42011939177 100644 --- a/cmd/nerdctl/apparmor_linux.go +++ b/cmd/nerdctl/apparmor/apparmor_linux.go @@ -14,18 +14,20 @@ limitations under the License. */ -package main +package apparmor import ( + "github.com/containerd/nerdctl/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/common" "github.com/spf13/cobra" ) -func newApparmorCommand() *cobra.Command { +func NewApparmorCommand() *cobra.Command { cmd := &cobra.Command{ - Annotations: map[string]string{Category: Management}, + Annotations: map[string]string{common.Category: common.Management}, Use: "apparmor", Short: "Manage AppArmor profiles", - RunE: unknownSubcommandAction, + RunE: completion.UnknownSubcommandAction, SilenceUsage: true, SilenceErrors: true, } @@ -37,3 +39,7 @@ func newApparmorCommand() *cobra.Command { ) return cmd } + +func AddApparmorCommand(rootCmd *cobra.Command) { + rootCmd.AddCommand(NewApparmorCommand()) +} diff --git a/cmd/nerdctl/apparmor_load_linux.go b/cmd/nerdctl/apparmor/apparmor_load_linux.go similarity index 91% rename from cmd/nerdctl/apparmor_load_linux.go rename to cmd/nerdctl/apparmor/apparmor_load_linux.go index f295faf795c..353ee6521f8 100644 --- a/cmd/nerdctl/apparmor_load_linux.go +++ b/cmd/nerdctl/apparmor/apparmor_load_linux.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package apparmor import ( "fmt" @@ -30,14 +30,14 @@ func newApparmorLoadCommand() *cobra.Command { Use: "load", Short: fmt.Sprintf("Load the default AppArmor profile %q. Requires root.", defaults.AppArmorProfileName), Args: cobra.NoArgs, - RunE: apparmorLoadAction, + RunE: LoadAction, SilenceUsage: true, SilenceErrors: true, } return cmd } -func apparmorLoadAction(cmd *cobra.Command, args []string) error { +func LoadAction(cmd *cobra.Command, args []string) error { logrus.Infof("Loading profile %q", defaults.AppArmorProfileName) return apparmor.LoadDefaultProfile(defaults.AppArmorProfileName) } diff --git a/cmd/nerdctl/apparmor_ls_linux.go b/cmd/nerdctl/apparmor/apparmor_ls_linux.go similarity index 91% rename from cmd/nerdctl/apparmor_ls_linux.go rename to cmd/nerdctl/apparmor/apparmor_ls_linux.go index 34a4ab1b92e..e802bb738fe 100644 --- a/cmd/nerdctl/apparmor_ls_linux.go +++ b/cmd/nerdctl/apparmor/apparmor_ls_linux.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package apparmor import ( "bytes" @@ -23,6 +23,7 @@ import ( "text/tabwriter" "text/template" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/fmtutil" "github.com/containerd/nerdctl/pkg/apparmorutil" "github.com/spf13/cobra" ) @@ -33,7 +34,7 @@ func newApparmorLsCommand() *cobra.Command { Aliases: []string{"list"}, Short: "List the loaded AppArmor profiles", Args: cobra.NoArgs, - RunE: apparmorLsAction, + RunE: LsAction, SilenceUsage: true, SilenceErrors: true, } @@ -46,7 +47,7 @@ func newApparmorLsCommand() *cobra.Command { return cmd } -func apparmorLsAction(cmd *cobra.Command, args []string) error { +func LsAction(cmd *cobra.Command, args []string) error { quiet, err := cmd.Flags().GetBool("quiet") if err != nil { return err @@ -70,7 +71,7 @@ func apparmorLsAction(cmd *cobra.Command, args []string) error { return errors.New("format and quiet must not be specified together") } var err error - tmpl, err = parseTemplate(format) + tmpl, err = fmtutil.ParseTemplate(format) if err != nil { return err } @@ -96,7 +97,7 @@ func apparmorLsAction(cmd *cobra.Command, args []string) error { fmt.Fprintf(w, "%s\t%s\n", f.Name, f.Mode) } } - if f, ok := w.(Flusher); ok { + if f, ok := w.(fmtutil.Flusher); ok { return f.Flush() } return nil diff --git a/cmd/nerdctl/apparmor_unload_linux.go b/cmd/nerdctl/apparmor/apparmor_unload_linux.go similarity index 79% rename from cmd/nerdctl/apparmor_unload_linux.go rename to cmd/nerdctl/apparmor/apparmor_unload_linux.go index 15154f3b95a..6b505f261d2 100644 --- a/cmd/nerdctl/apparmor_unload_linux.go +++ b/cmd/nerdctl/apparmor/apparmor_unload_linux.go @@ -14,11 +14,12 @@ limitations under the License. */ -package main +package apparmor import ( "fmt" + "github.com/containerd/nerdctl/cmd/nerdctl/completion" "github.com/containerd/nerdctl/pkg/apparmorutil" "github.com/containerd/nerdctl/pkg/defaults" "github.com/sirupsen/logrus" @@ -30,15 +31,15 @@ func newApparmorUnloadCommand() *cobra.Command { Use: "unload [PROFILE]", Short: fmt.Sprintf("Unload an AppArmor profile. The target profile name defaults to %q. Requires root.", defaults.AppArmorProfileName), Args: cobra.MaximumNArgs(1), - RunE: apparmorUnloadAction, - ValidArgsFunction: apparmorUnloadShellComplete, + RunE: UnloadAction, + ValidArgsFunction: completion.ApparmorUnloadShellComplete, SilenceUsage: true, SilenceErrors: true, } return cmd } -func apparmorUnloadAction(cmd *cobra.Command, args []string) error { +func UnloadAction(cmd *cobra.Command, args []string) error { target := defaults.AppArmorProfileName if len(args) > 0 { target = args[0] @@ -46,7 +47,3 @@ func apparmorUnloadAction(cmd *cobra.Command, args []string) error { logrus.Infof("Unloading profile %q", target) return apparmorutil.Unload(target) } - -func apparmorUnloadShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return shellCompleteApparmorProfiles(cmd) -} diff --git a/cmd/nerdctl/apparmor/apparmor_windows.go b/cmd/nerdctl/apparmor/apparmor_windows.go new file mode 100644 index 00000000000..e369b44d0e8 --- /dev/null +++ b/cmd/nerdctl/apparmor/apparmor_windows.go @@ -0,0 +1,23 @@ +/* + 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 apparmor + +import "github.com/spf13/cobra" + +func AddApparmorCommand(rootCmd *cobra.Command) { + // NOP +} diff --git a/cmd/nerdctl/build.go b/cmd/nerdctl/builder/build.go similarity index 93% rename from cmd/nerdctl/build.go rename to cmd/nerdctl/builder/build.go index b85900a5f82..d699e0fd272 100644 --- a/cmd/nerdctl/build.go +++ b/cmd/nerdctl/builder/build.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package builder import ( "encoding/json" @@ -30,6 +30,9 @@ import ( "github.com/containerd/containerd/errdefs" dockerreference "github.com/containerd/containerd/reference/docker" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/cmd/nerdctl/utils" "github.com/containerd/nerdctl/pkg/buildkitutil" "github.com/containerd/nerdctl/pkg/defaults" "github.com/containerd/nerdctl/pkg/platformutil" @@ -39,7 +42,7 @@ import ( "github.com/spf13/cobra" ) -func newBuildCommand() *cobra.Command { +func NewBuildCommand() *cobra.Command { var buildCommand = &cobra.Command{ Use: "build [flags] PATH", Short: "Build an image from a Dockerfile. Needs buildkitd to be running.", @@ -49,7 +52,7 @@ If Dockerfile is not present and -f is not specified, it will look for Container SilenceUsage: true, SilenceErrors: true, } - AddStringFlag(buildCommand, "buildkit-host", nil, defaults.BuildKitHost(), "BUILDKIT_HOST", "BuildKit address") + utils.AddStringFlag(buildCommand, "buildkit-host", nil, defaults.BuildKitHost(), "BUILDKIT_HOST", "BuildKit address") buildCommand.Flags().StringArrayP("tag", "t", nil, "Name and optionally a tag in the 'name:tag' format") buildCommand.Flags().StringP("file", "f", "", "Name of the Dockerfile") buildCommand.Flags().String("target", "", "Set the target build stage to build") @@ -67,7 +70,7 @@ If Dockerfile is not present and -f is not specified, it will look for Container // #region platform flags // platform is defined as StringSlice, not StringArray, to allow specifying "--platform=amd64,arm64" buildCommand.Flags().StringSlice("platform", []string{}, "Set target platform for build (e.g., \"amd64\", \"arm64\")") - buildCommand.RegisterFlagCompletionFunc("platform", shellCompletePlatforms) + buildCommand.RegisterFlagCompletionFunc("platform", completion.ShellCompletePlatforms) // #endregion buildCommand.Flags().Bool("ipfs", false, "Allow pulling base images from IPFS") @@ -77,7 +80,7 @@ If Dockerfile is not present and -f is not specified, it will look for Container return buildCommand } -func getBuildkitHost(cmd *cobra.Command) (string, error) { +func BuildkitHost(cmd *cobra.Command) (string, error) { if cmd.Flags().Changed("buildkit-host") || os.Getenv("BUILDKIT_HOST") != "" { // If address is explicitly specified, use it. buildkitHost, err := cmd.Flags().GetString("buildkit-host") @@ -132,7 +135,7 @@ func buildAction(cmd *cobra.Command, args []string) error { } platform = strutil.DedupeStrSlice(platform) - buildkitHost, err := getBuildkitHost(cmd) + buildkitHost, err := BuildkitHost(cmd) if err != nil { return err } @@ -151,7 +154,7 @@ func buildAction(cmd *cobra.Command, args []string) error { } if runIPFSRegistry { logrus.Infof("Ensuring IPFS registry is running") - nerdctlCmd, nerdctlArgs := globalFlags(cmd) + nerdctlCmd, nerdctlArgs := utils.GlobalFlags(cmd) out, err := exec.Command(nerdctlCmd, append(nerdctlArgs, "ipfs", "registry", "up")...).CombinedOutput() if err != nil { return fmt.Errorf("failed to start IPFS registry: %v: %v", string(out), err) @@ -190,7 +193,7 @@ func buildAction(cmd *cobra.Command, args []string) error { if err != nil { return err } - if err = loadImage(buildctlStdout, cmd, platMC, quiet); err != nil { + if err = utils.LoadImage(buildctlStdout, cmd, platMC, quiet); err != nil { return err } } @@ -212,7 +215,7 @@ func buildAction(cmd *cobra.Command, args []string) error { if len(tags) > 1 { logrus.Debug("Found more than 1 tag") - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return fmt.Errorf("unable to tag images: %s", err) } @@ -257,7 +260,7 @@ func generateBuildctlArgs(cmd *cobra.Command, buildkitHost string, platform, arg return "", nil, false, "", nil, nil, err } if output == "" { - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return "", nil, false, "", nil, nil, err } @@ -499,6 +502,17 @@ func generateBuildctlArgs(cmd *cobra.Command, buildkitHost string, platform, arg return buildctlBinary, buildctlArgs, needsLoading, metaFile, tags, cleanup, nil } +func CreateBuildContext(dockerfile string) (string, error) { + tmpDir, err := os.MkdirTemp("", "nerdctl-build-test") + if err != nil { + return "", err + } + if err = os.WriteFile(filepath.Join(tmpDir, "Dockerfile"), []byte(dockerfile), 0644); err != nil { + return "", err + } + return tmpDir, nil +} + func getDigestFromMetaFile(path string) (string, error) { data, err := os.ReadFile(path) if err != nil { diff --git a/cmd/nerdctl/builder.go b/cmd/nerdctl/builder/builder.go similarity index 87% rename from cmd/nerdctl/builder.go rename to cmd/nerdctl/builder/builder.go index ba9d265ef8b..c3c2d2c6c92 100644 --- a/cmd/nerdctl/builder.go +++ b/cmd/nerdctl/builder/builder.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package builder import ( "fmt" @@ -22,29 +22,33 @@ import ( "os/exec" "strings" + "github.com/containerd/nerdctl/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/cmd/nerdctl/utils" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/common" "github.com/containerd/nerdctl/pkg/buildkitutil" "github.com/containerd/nerdctl/pkg/defaults" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) -func newBuilderCommand() *cobra.Command { +func NewBuilderCommand() *cobra.Command { var builderCommand = &cobra.Command{ - Annotations: map[string]string{Category: Management}, + Annotations: map[string]string{common.Category: common.Management}, Use: "builder", Short: "Manage builds", - RunE: unknownSubcommandAction, + RunE: completion.UnknownSubcommandAction, SilenceUsage: true, SilenceErrors: true, } builderCommand.AddCommand( - newBuilderPruneCommand(), - newBuilderDebugCommand(), + NewBuildCommand(), + NewPruneCommand(), + NewDebugCommand(), ) return builderCommand } -func newBuilderPruneCommand() *cobra.Command { +func NewPruneCommand() *cobra.Command { shortHelp := `Clean up BuildKit build cache` var buildPruneCommand = &cobra.Command{ Use: "prune", @@ -55,12 +59,12 @@ func newBuilderPruneCommand() *cobra.Command { SilenceErrors: true, } - AddStringFlag(buildPruneCommand, "buildkit-host", nil, defaults.BuildKitHost(), "BUILDKIT_HOST", "BuildKit address") + utils.AddStringFlag(buildPruneCommand, "buildkit-host", nil, defaults.BuildKitHost(), "BUILDKIT_HOST", "BuildKit address") return buildPruneCommand } func builderPruneAction(cmd *cobra.Command, _ []string) error { - buildkitHost, err := getBuildkitHost(cmd) + buildkitHost, err := BuildkitHost(cmd) if err != nil { return err } @@ -77,12 +81,12 @@ func builderPruneAction(cmd *cobra.Command, _ []string) error { return buildctlCmd.Run() } -func newBuilderDebugCommand() *cobra.Command { +func NewDebugCommand() *cobra.Command { shortHelp := `Debug Dockerfile` var buildDebugCommand = &cobra.Command{ Use: "debug", Short: shortHelp, - PreRunE: checkExperimental("`nerdctl builder debug`"), + PreRunE: utils.CheckExperimental("`nerdctl builder debug`"), RunE: builderDebugAction, SilenceUsage: true, SilenceErrors: true, diff --git a/cmd/nerdctl/client.go b/cmd/nerdctl/client/client.go similarity index 88% rename from cmd/nerdctl/client.go rename to cmd/nerdctl/client/client.go index ea7f90e26ac..50bb3f37baf 100644 --- a/cmd/nerdctl/client.go +++ b/cmd/nerdctl/client/client.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package client import ( "context" @@ -35,7 +35,7 @@ import ( "github.com/opencontainers/go-digest" ) -func newClient(cmd *cobra.Command, opts ...containerd.ClientOpt) (*containerd.Client, context.Context, context.CancelFunc, error) { +func New(cmd *cobra.Command, opts ...containerd.ClientOpt) (*containerd.Client, context.Context, context.CancelFunc, error) { ctx := cmd.Context() namespace, err := cmd.Flags().GetString("namespace") if err != nil { @@ -65,7 +65,7 @@ func newClient(cmd *cobra.Command, opts ...containerd.ClientOpt) (*containerd.Cl return client, ctx, cancel, nil } -func newClientWithPlatform(cmd *cobra.Command, platform string, clientOpts ...containerd.ClientOpt) (*containerd.Client, context.Context, context.CancelFunc, error) { +func NewWithPlatform(cmd *cobra.Command, platform string, clientOpts ...containerd.ClientOpt) (*containerd.Client, context.Context, context.CancelFunc, error) { if platform != "" { if canExec, canExecErr := platformutil.CanExecProbably(platform); !canExec { warn := fmt.Sprintf("Platform %q seems incompatible with the host platform %q. If you see \"exec format error\", see https://github.com/containerd/nerdctl/blob/main/docs/multi-platform.md", @@ -83,13 +83,13 @@ func newClientWithPlatform(cmd *cobra.Command, platform string, clientOpts ...co platformM := platforms.Only(platformParsed) clientOpts = append(clientOpts, containerd.WithDefaultPlatform(platformM)) } - return newClient(cmd, clientOpts...) + return New(cmd, clientOpts...) } -// getDataStore returns a string like "/var/lib/nerdctl/1935db59". +// DataStore returns a string like "/var/lib/nerdctl/1935db59". // "1935db9" is from `$(echo -n "/run/containerd/containerd.sock" | sha256sum | cut -c1-8)` // on Windows it will return "%PROGRAMFILES%/nerdctl/1935db59" -func getDataStore(cmd *cobra.Command) (string, error) { +func DataStore(cmd *cobra.Command) (string, error) { dataRoot, err := cmd.Flags().GetString("data-root") if err != nil { return "", err diff --git a/cmd/nerdctl/completion.go b/cmd/nerdctl/completion/completion.go similarity index 51% rename from cmd/nerdctl/completion.go rename to cmd/nerdctl/completion/completion.go index d86ce014696..151bb405a22 100644 --- a/cmd/nerdctl/completion.go +++ b/cmd/nerdctl/completion/completion.go @@ -14,20 +14,24 @@ limitations under the License. */ -package main +package completion import ( "context" + "errors" + "fmt" "time" "github.com/containerd/containerd" + "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/volume" "github.com/containerd/nerdctl/pkg/labels" "github.com/containerd/nerdctl/pkg/netutil" "github.com/spf13/cobra" ) -func shellCompleteImageNames(cmd *cobra.Command) ([]string, cobra.ShellCompDirective) { - client, ctx, cancel, err := newClient(cmd) +func ShellCompleteImageNames(cmd *cobra.Command) ([]string, cobra.ShellCompDirective) { + client, ctx, cancel, err := client.New(cmd) if err != nil { return nil, cobra.ShellCompDirectiveError } @@ -45,8 +49,8 @@ func shellCompleteImageNames(cmd *cobra.Command) ([]string, cobra.ShellCompDirec return candidates, cobra.ShellCompDirectiveNoFileComp } -func shellCompleteContainerNames(cmd *cobra.Command, filterFunc func(containerd.ProcessStatus) bool) ([]string, cobra.ShellCompDirective) { - client, ctx, cancel, err := newClient(cmd) +func ShellCompleteContainerNames(cmd *cobra.Command, filterFunc func(containerd.ProcessStatus) bool) ([]string, cobra.ShellCompDirective) { + client, ctx, cancel, err := client.New(cmd) if err != nil { return nil, cobra.ShellCompDirectiveError } @@ -89,8 +93,8 @@ func shellCompleteContainerNames(cmd *cobra.Command, filterFunc func(containerd. return candidates, cobra.ShellCompDirectiveNoFileComp } -// shellCompleteNetworkNames includes {"bridge","host","none"} -func shellCompleteNetworkNames(cmd *cobra.Command, exclude []string) ([]string, cobra.ShellCompDirective) { +// ShellCompleteNetworkNames includes {"bridge","host","none"} +func ShellCompleteNetworkNames(cmd *cobra.Command, exclude []string) ([]string, cobra.ShellCompDirective) { excludeMap := make(map[string]struct{}, len(exclude)) for _, ex := range exclude { excludeMap[ex] = struct{}{} @@ -126,8 +130,8 @@ func shellCompleteNetworkNames(cmd *cobra.Command, exclude []string) ([]string, return candidates, cobra.ShellCompDirectiveNoFileComp } -func shellCompleteVolumeNames(cmd *cobra.Command) ([]string, cobra.ShellCompDirective) { - vols, err := getVolumes(cmd) +func ShellCompleteVolumeNames(cmd *cobra.Command) ([]string, cobra.ShellCompDirective) { + vols, err := volume.Volumes(cmd) if err != nil { return nil, cobra.ShellCompDirectiveError } @@ -138,7 +142,7 @@ func shellCompleteVolumeNames(cmd *cobra.Command) ([]string, cobra.ShellCompDire return candidates, cobra.ShellCompDirectiveNoFileComp } -func shellCompletePlatforms(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { +func ShellCompletePlatforms(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { candidates := []string{ "amd64", "arm64", @@ -151,3 +155,75 @@ func shellCompletePlatforms(cmd *cobra.Command, args []string, toComplete string } return candidates, cobra.ShellCompDirectiveNoFileComp } + +func TopShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + // show running container names + statusFilterFn := func(st containerd.ProcessStatus) bool { + return st == containerd.Running + } + return ShellCompleteContainerNames(cmd, statusFilterFn) +} + +func PauseShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + // show running container names + statusFilterFn := func(st containerd.ProcessStatus) bool { + return st == containerd.Running + } + return ShellCompleteContainerNames(cmd, statusFilterFn) +} + +func UnpauseShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + // show paused container names + statusFilterFn := func(st containerd.ProcessStatus) bool { + return st == containerd.Paused + } + return ShellCompleteContainerNames(cmd, statusFilterFn) +} + +func LogsShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + // show container names (TODO: only show Containers with logs) + return ShellCompleteContainerNames(cmd, nil) +} + +func StartShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + // show non-running container names + statusFilterFn := func(st containerd.ProcessStatus) bool { + return st != containerd.Running && st != containerd.Unknown + } + return ShellCompleteContainerNames(cmd, statusFilterFn) +} + +func StatsShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + // show running container names + statusFilterFn := func(st containerd.ProcessStatus) bool { + return st == containerd.Running + } + return ShellCompleteContainerNames(cmd, statusFilterFn) +} + +func StopShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + // show non-stopped container names + statusFilterFn := func(st containerd.ProcessStatus) bool { + return st != containerd.Stopped && st != containerd.Created && st != containerd.Unknown + } + return ShellCompleteContainerNames(cmd, statusFilterFn) +} + +// UnknownSubcommandAction is needed to let `nerdctl system non-existent-command` fail +// https://github.com/containerd/nerdctl/issues/487 +// +// Ideally this should be implemented in Cobra itself. +func UnknownSubcommandAction(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return cmd.Help() + } + // The output mimics https://github.com/spf13/cobra/blob/v1.2.1/command.go#L647-L662 + msg := fmt.Sprintf("unknown subcommand %q for %q", args[0], cmd.Name()) + if suggestions := cmd.SuggestionsFor(args[0]); len(suggestions) > 0 { + msg += "\n\nDid you mean this?\n" + for _, s := range suggestions { + msg += fmt.Sprintf("\t%v\n", s) + } + } + return errors.New(msg) +} diff --git a/cmd/nerdctl/completion/completion_freebsd.go b/cmd/nerdctl/completion/completion_freebsd.go new file mode 100644 index 00000000000..7745811be05 --- /dev/null +++ b/cmd/nerdctl/completion/completion_freebsd.go @@ -0,0 +1,32 @@ +/* + 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 completion + +import "github.com/spf13/cobra" + +func CapShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + candidates := []string{} + return candidates, cobra.ShellCompDirectiveNoFileComp +} + +func RunShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return nil, cobra.ShellCompDirectiveNoFileComp +} + +func ShellCompleteCgroupManagerNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return nil, cobra.ShellCompDirectiveNoFileComp +} diff --git a/cmd/nerdctl/completion/completion_linux.go b/cmd/nerdctl/completion/completion_linux.go new file mode 100644 index 00000000000..65d75e3dcc9 --- /dev/null +++ b/cmd/nerdctl/completion/completion_linux.go @@ -0,0 +1,71 @@ +/* + 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 completion + +import ( + "strings" + + "github.com/containerd/containerd/pkg/cap" + "github.com/containerd/nerdctl/pkg/apparmorutil" + "github.com/containerd/nerdctl/pkg/defaults" + "github.com/containerd/nerdctl/pkg/rootlessutil" + "github.com/spf13/cobra" +) + +func ShellCompleteApparmorProfiles(cmd *cobra.Command) ([]string, cobra.ShellCompDirective) { + profiles, err := apparmorutil.Profiles() + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + var names []string // nolint: prealloc + for _, f := range profiles { + names = append(names, f.Name) + } + return names, cobra.ShellCompDirectiveNoFileComp +} + +func CapShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + candidates := []string{} + for _, c := range cap.Known() { + // "CAP_SYS_ADMIN" -> "sys_admin" + s := strings.ToLower(strings.TrimPrefix(c, "CAP_")) + candidates = append(candidates, s) + } + return candidates, cobra.ShellCompDirectiveNoFileComp +} + +func RunShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) == 0 { + return ShellCompleteImageNames(cmd) + } + return nil, cobra.ShellCompDirectiveNoFileComp +} + +func ShellCompleteCgroupManagerNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + candidates := []string{"cgroupfs"} + if defaults.IsSystemdAvailable() { + candidates = append(candidates, "systemd") + } + if rootlessutil.IsRootless() { + candidates = append(candidates, "none") + } + return candidates, cobra.ShellCompDirectiveNoFileComp +} + +func ApparmorUnloadShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return ShellCompleteApparmorProfiles(cmd) +} diff --git a/cmd/nerdctl/completion/completion_unix.go b/cmd/nerdctl/completion/completion_unix.go new file mode 100644 index 00000000000..120ea83616d --- /dev/null +++ b/cmd/nerdctl/completion/completion_unix.go @@ -0,0 +1,68 @@ +//go:build freebsd || linux + +/* + 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 completion + +import ( + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/pkg/infoutil" + "github.com/containerd/nerdctl/pkg/rootlessutil" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +func ShellCompleteNamespaceNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if rootlessutil.IsRootlessParent() { + _ = rootlessutil.ParentMain() + return nil, cobra.ShellCompDirectiveNoFileComp + } + + client, ctx, cancel, err := ncclient.New(cmd) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + defer cancel() + nsService := client.NamespaceService() + nsList, err := nsService.List(ctx) + if err != nil { + logrus.Warn(err) + return nil, cobra.ShellCompDirectiveError + } + var candidates []string + candidates = append(candidates, nsList...) + return candidates, cobra.ShellCompDirectiveNoFileComp +} + +func ShellCompleteSnapshotterNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if rootlessutil.IsRootlessParent() { + _ = rootlessutil.ParentMain() + return nil, cobra.ShellCompDirectiveNoFileComp + } + client, ctx, cancel, err := ncclient.New(cmd) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + defer cancel() + snapshotterPlugins, err := infoutil.GetSnapshotterNames(ctx, client.IntrospectionService()) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + var candidates []string + candidates = append(candidates, snapshotterPlugins...) + return candidates, cobra.ShellCompDirectiveNoFileComp +} diff --git a/cmd/nerdctl/completion/completion_windows.go b/cmd/nerdctl/completion/completion_windows.go new file mode 100644 index 00000000000..0cbbf5c546c --- /dev/null +++ b/cmd/nerdctl/completion/completion_windows.go @@ -0,0 +1,40 @@ +/* + 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 completion + +import "github.com/spf13/cobra" + +func CapShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + candidates := []string{} + return candidates, cobra.ShellCompDirectiveNoFileComp +} + +func RunShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return nil, cobra.ShellCompDirectiveNoFileComp +} + +func ShellCompleteNamespaceNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return nil, cobra.ShellCompDirectiveNoFileComp +} + +func ShellCompleteSnapshotterNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return nil, cobra.ShellCompDirectiveNoFileComp +} + +func ShellCompleteCgroupManagerNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return nil, cobra.ShellCompDirectiveNoFileComp +} diff --git a/cmd/nerdctl/compose.go b/cmd/nerdctl/compose/compose.go similarity index 93% rename from cmd/nerdctl/compose.go rename to cmd/nerdctl/compose/compose.go index bf78a49dc63..8ab6fa52f02 100644 --- a/cmd/nerdctl/compose.go +++ b/cmd/nerdctl/compose/compose.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package compose import ( "context" @@ -24,6 +24,9 @@ import ( "github.com/containerd/containerd" "github.com/containerd/containerd/errdefs" "github.com/containerd/containerd/platforms" + "github.com/containerd/nerdctl/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/cmd/nerdctl/utils" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/volume" "github.com/containerd/nerdctl/pkg/composer" "github.com/containerd/nerdctl/pkg/composer/serviceparser" "github.com/containerd/nerdctl/pkg/cosignutil" @@ -38,17 +41,17 @@ import ( "github.com/spf13/cobra" ) -func newComposeCommand() *cobra.Command { +func NewComposeCommand() *cobra.Command { var composeCommand = &cobra.Command{ Use: "compose [flags] COMMAND", Short: "Compose", - RunE: unknownSubcommandAction, + RunE: completion.UnknownSubcommandAction, SilenceUsage: true, SilenceErrors: true, TraverseChildren: true, // required for global short hands like -f } // `-f` is a nonPersistentAlias, as it conflicts with `nerdctl compose logs --follow` - AddPersistentStringArrayFlag(composeCommand, "file", nil, []string{"f"}, nil, "", "Specify an alternate compose file") + utils.AddPersistentStringArrayFlag(composeCommand, "file", nil, []string{"f"}, nil, "", "Specify an alternate compose file") composeCommand.PersistentFlags().String("project-directory", "", "Specify an alternate working directory") composeCommand.PersistentFlags().StringP("project-name", "p", "", "Specify an alternate project name") composeCommand.PersistentFlags().String("env-file", "", "Specify an alternate environment file") @@ -80,7 +83,7 @@ func newComposeCommand() *cobra.Command { } func getComposer(cmd *cobra.Command, client *containerd.Client) (*composer.Composer, error) { - nerdctlCmd, nerdctlArgs := globalFlags(cmd) + nerdctlCmd, nerdctlArgs := utils.GlobalFlags(cmd) projectDirectory, err := cmd.Flags().GetString("project-directory") if err != nil { return nil, err @@ -155,7 +158,7 @@ func getComposer(cmd *cobra.Command, client *containerd.Client) (*composer.Compo return false, nil } - volStore, err := getVolumeStore(cmd) + volStore, err := volume.Store(cmd) if err != nil { return nil, err } diff --git a/cmd/nerdctl/compose_build.go b/cmd/nerdctl/compose/compose_build.go similarity index 94% rename from cmd/nerdctl/compose_build.go rename to cmd/nerdctl/compose/compose_build.go index b08032581b5..660cee1440a 100644 --- a/cmd/nerdctl/compose_build.go +++ b/cmd/nerdctl/compose/compose_build.go @@ -14,9 +14,10 @@ limitations under the License. */ -package main +package compose import ( + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" "github.com/containerd/nerdctl/pkg/composer" "github.com/spf13/cobra" ) @@ -56,7 +57,7 @@ func composeBuildAction(cmd *cobra.Command, args []string) error { return err } - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } diff --git a/cmd/nerdctl/compose_config.go b/cmd/nerdctl/compose/compose_config.go similarity index 95% rename from cmd/nerdctl/compose_config.go rename to cmd/nerdctl/compose/compose_config.go index 17fa15746e6..54587d7466d 100644 --- a/cmd/nerdctl/compose_config.go +++ b/cmd/nerdctl/compose/compose_config.go @@ -14,11 +14,12 @@ limitations under the License. */ -package main +package compose import ( "fmt" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" "github.com/containerd/nerdctl/pkg/composer" "github.com/spf13/cobra" ) @@ -63,7 +64,7 @@ func composeConfigAction(cmd *cobra.Command, args []string) error { return err } - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } diff --git a/cmd/nerdctl/compose_down.go b/cmd/nerdctl/compose/compose_down.go similarity index 92% rename from cmd/nerdctl/compose_down.go rename to cmd/nerdctl/compose/compose_down.go index b779ff13ea7..8238b742dee 100644 --- a/cmd/nerdctl/compose_down.go +++ b/cmd/nerdctl/compose/compose_down.go @@ -14,9 +14,10 @@ limitations under the License. */ -package main +package compose import ( + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" "github.com/containerd/nerdctl/pkg/composer" "github.com/spf13/cobra" ) @@ -39,7 +40,7 @@ func composeDownAction(cmd *cobra.Command, args []string) error { if err != nil { return err } - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } diff --git a/cmd/nerdctl/compose_exec.go b/cmd/nerdctl/compose/compose_exec.go similarity index 96% rename from cmd/nerdctl/compose_exec.go rename to cmd/nerdctl/compose/compose_exec.go index 0dbcd0ebaf3..ff53c4e9b1b 100644 --- a/cmd/nerdctl/compose_exec.go +++ b/cmd/nerdctl/compose/compose_exec.go @@ -14,11 +14,12 @@ limitations under the License. */ -package main +package compose import ( "errors" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" "github.com/containerd/nerdctl/pkg/composer" "github.com/spf13/cobra" ) @@ -94,7 +95,7 @@ func composeExecAction(cmd *cobra.Command, args []string) error { return errors.New("currently flag -t and -d cannot be specified together (FIXME)") } - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } diff --git a/cmd/nerdctl/compose_images.go b/cmd/nerdctl/compose/compose_images.go similarity index 94% rename from cmd/nerdctl/compose_images.go rename to cmd/nerdctl/compose/compose_images.go index e5b52cda1a1..251d7327c0b 100644 --- a/cmd/nerdctl/compose_images.go +++ b/cmd/nerdctl/compose/compose_images.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package compose import ( "context" @@ -25,6 +25,8 @@ import ( "github.com/containerd/containerd" "github.com/containerd/containerd/pkg/progress" "github.com/containerd/containerd/snapshots" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/utils" "github.com/containerd/nerdctl/pkg/imgutil" "github.com/containerd/nerdctl/pkg/labels" "github.com/containerd/nerdctl/pkg/strutil" @@ -51,7 +53,7 @@ func composeImagesAction(cmd *cobra.Command, args []string) error { return err } - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } @@ -131,7 +133,7 @@ func printComposeImages(ctx context.Context, cmd *cobra.Command, containers []co return err } - size, err := unpackedImageSize(ctx, sn, image) + size, err := utils.UnpackedImageSize(ctx, sn, image) if err != nil { return err } diff --git a/cmd/nerdctl/compose_kill.go b/cmd/nerdctl/compose/compose_kill.go similarity index 92% rename from cmd/nerdctl/compose_kill.go rename to cmd/nerdctl/compose/compose_kill.go index 6ab4d152d87..5f8978bfb87 100644 --- a/cmd/nerdctl/compose_kill.go +++ b/cmd/nerdctl/compose/compose_kill.go @@ -14,9 +14,10 @@ limitations under the License. */ -package main +package compose import ( + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" "github.com/containerd/nerdctl/pkg/composer" "github.com/spf13/cobra" ) @@ -38,7 +39,7 @@ func composeKillAction(cmd *cobra.Command, args []string) error { if err != nil { return err } - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } diff --git a/cmd/nerdctl/compose_logs.go b/cmd/nerdctl/compose/compose_logs.go similarity index 94% rename from cmd/nerdctl/compose_logs.go rename to cmd/nerdctl/compose/compose_logs.go index 204138dcb39..a1cce7476fe 100644 --- a/cmd/nerdctl/compose_logs.go +++ b/cmd/nerdctl/compose/compose_logs.go @@ -14,9 +14,10 @@ limitations under the License. */ -package main +package compose import ( + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" "github.com/containerd/nerdctl/pkg/composer" "github.com/spf13/cobra" ) @@ -59,7 +60,7 @@ func composeLogsAction(cmd *cobra.Command, args []string) error { return err } - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } diff --git a/cmd/nerdctl/compose_pause.go b/cmd/nerdctl/compose/compose_pause.go similarity index 89% rename from cmd/nerdctl/compose_pause.go rename to cmd/nerdctl/compose/compose_pause.go index 031746252cc..c536be7190c 100644 --- a/cmd/nerdctl/compose_pause.go +++ b/cmd/nerdctl/compose/compose_pause.go @@ -14,13 +14,15 @@ limitations under the License. */ -package main +package compose import ( "fmt" "sync" "github.com/containerd/containerd" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/action" "github.com/containerd/nerdctl/pkg/labels" "github.com/spf13/cobra" "golang.org/x/sync/errgroup" @@ -39,7 +41,7 @@ func newComposePauseCommand() *cobra.Command { } func composePauseAction(cmd *cobra.Command, args []string) error { - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } @@ -65,7 +67,7 @@ func composePauseAction(cmd *cobra.Command, args []string) error { for _, c := range containers { c := c eg.Go(func() error { - if err := pauseContainer(ctx, client, c.ID()); err != nil { + if err := action.PauseContainer(ctx, client, c.ID()); err != nil { return err } info, err := c.Info(ctx, containerd.WithoutRefreshedMetadata) @@ -97,7 +99,7 @@ func newComposeUnpauseCommand() *cobra.Command { } func composeUnpauseAction(cmd *cobra.Command, args []string) error { - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } @@ -123,7 +125,7 @@ func composeUnpauseAction(cmd *cobra.Command, args []string) error { for _, c := range containers { c := c eg.Go(func() error { - if err := unpauseContainer(ctx, client, c.ID()); err != nil { + if err := action.UnpauseContainer(ctx, client, c.ID()); err != nil { return err } info, err := c.Info(ctx, containerd.WithoutRefreshedMetadata) diff --git a/cmd/nerdctl/compose_port.go b/cmd/nerdctl/compose/compose_port.go similarity index 94% rename from cmd/nerdctl/compose_port.go rename to cmd/nerdctl/compose/compose_port.go index e651eeaf6f5..3430c4085c3 100644 --- a/cmd/nerdctl/compose_port.go +++ b/cmd/nerdctl/compose/compose_port.go @@ -14,12 +14,13 @@ limitations under the License. */ -package main +package compose import ( "fmt" "strconv" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" "github.com/containerd/nerdctl/pkg/composer" "github.com/spf13/cobra" ) @@ -66,7 +67,7 @@ func composePortAction(cmd *cobra.Command, args []string) error { return fmt.Errorf("unexpected port: %d", port) } - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } diff --git a/cmd/nerdctl/compose_ps.go b/cmd/nerdctl/compose/compose_ps.go similarity index 85% rename from cmd/nerdctl/compose_ps.go rename to cmd/nerdctl/compose/compose_ps.go index f5fd69cbc3c..17d01dee19c 100644 --- a/cmd/nerdctl/compose_ps.go +++ b/cmd/nerdctl/compose/compose_ps.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package compose import ( "context" @@ -24,6 +24,7 @@ import ( "github.com/containerd/containerd" gocni "github.com/containerd/go-cni" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" "github.com/containerd/nerdctl/pkg/formatter" "github.com/containerd/nerdctl/pkg/labels" "github.com/containerd/nerdctl/pkg/portutil" @@ -44,7 +45,7 @@ func newComposePsCommand() *cobra.Command { return composePsCommand } -type composeContainerPrintable struct { +type ContainerPrintable struct { ID string Name string Command string @@ -68,7 +69,7 @@ func composePsAction(cmd *cobra.Command, args []string) error { return fmt.Errorf("unsupported format %s, supported formats are: [json]", format) } - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } @@ -87,17 +88,17 @@ func composePsAction(cmd *cobra.Command, args []string) error { return err } - containersPrintable := make([]composeContainerPrintable, len(containers)) + containersPrintable := make([]ContainerPrintable, len(containers)) eg, ctx := errgroup.WithContext(ctx) for i, container := range containers { i, container := i, container eg.Go(func() error { - var p composeContainerPrintable + var p ContainerPrintable var err error if format == "json" { - p, err = composeContainerPrintableJSON(ctx, container) + p, err = ContainerPrintableJSON(ctx, container) } else { - p, err = composeContainerPrintableTab(ctx, container) + p, err = ContainerPrintableTab(ctx, container) } if err != nil { return err @@ -137,23 +138,23 @@ func composePsAction(cmd *cobra.Command, args []string) error { return w.Flush() } -// composeContainerPrintableTab constructs composeContainerPrintable with fields +// ContainerPrintableTab constructs ContainerPrintable with fields // only for console output. -func composeContainerPrintableTab(ctx context.Context, container containerd.Container) (composeContainerPrintable, error) { +func ContainerPrintableTab(ctx context.Context, container containerd.Container) (ContainerPrintable, error) { info, err := container.Info(ctx, containerd.WithoutRefreshedMetadata) if err != nil { - return composeContainerPrintable{}, err + return ContainerPrintable{}, err } spec, err := container.Spec(ctx) if err != nil { - return composeContainerPrintable{}, err + return ContainerPrintable{}, err } status := formatter.ContainerStatus(ctx, container) if status == "Up" { status = "running" // corresponds to Docker Compose v2.0.1 } - return composeContainerPrintable{ + return ContainerPrintable{ Name: info.Labels[labels.Name], Command: formatter.InspectContainerCommandTrunc(spec), Service: info.Labels[labels.ComposeService], @@ -162,16 +163,16 @@ func composeContainerPrintableTab(ctx context.Context, container containerd.Cont }, nil } -// composeContainerPrintableTab constructs composeContainerPrintable with fields +// ContainerPrintableTab constructs ContainerPrintable with fields // only for json output and compatible docker output. -func composeContainerPrintableJSON(ctx context.Context, container containerd.Container) (composeContainerPrintable, error) { +func ContainerPrintableJSON(ctx context.Context, container containerd.Container) (ContainerPrintable, error) { info, err := container.Info(ctx, containerd.WithoutRefreshedMetadata) if err != nil { - return composeContainerPrintable{}, err + return ContainerPrintable{}, err } spec, err := container.Spec(ctx) if err != nil { - return composeContainerPrintable{}, err + return ContainerPrintable{}, err } var ( @@ -189,7 +190,7 @@ func composeContainerPrintableJSON(ctx context.Context, container containerd.Con state = string(containerd.Unknown) } - return composeContainerPrintable{ + return ContainerPrintable{ ID: container.ID(), Name: info.Labels[labels.Name], Command: formatter.InspectContainerCommand(spec, false, false), diff --git a/cmd/nerdctl/compose_pull.go b/cmd/nerdctl/compose/compose_pull.go similarity index 91% rename from cmd/nerdctl/compose_pull.go rename to cmd/nerdctl/compose/compose_pull.go index 8976477370f..035a070750e 100644 --- a/cmd/nerdctl/compose_pull.go +++ b/cmd/nerdctl/compose/compose_pull.go @@ -14,9 +14,10 @@ limitations under the License. */ -package main +package compose import ( + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" "github.com/containerd/nerdctl/pkg/composer" "github.com/spf13/cobra" ) @@ -34,7 +35,7 @@ func newComposePullCommand() *cobra.Command { } func composePullAction(cmd *cobra.Command, args []string) error { - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } diff --git a/cmd/nerdctl/compose_push.go b/cmd/nerdctl/compose/compose_push.go similarity index 90% rename from cmd/nerdctl/compose_push.go rename to cmd/nerdctl/compose/compose_push.go index 36a2f4f2aae..e1da041b164 100644 --- a/cmd/nerdctl/compose_push.go +++ b/cmd/nerdctl/compose/compose_push.go @@ -14,9 +14,10 @@ limitations under the License. */ -package main +package compose import ( + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" "github.com/containerd/nerdctl/pkg/composer" "github.com/spf13/cobra" ) @@ -33,7 +34,7 @@ func newComposePushCommand() *cobra.Command { } func composePushAction(cmd *cobra.Command, args []string) error { - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } diff --git a/cmd/nerdctl/compose_restart.go b/cmd/nerdctl/compose/compose_restart.go similarity index 92% rename from cmd/nerdctl/compose_restart.go rename to cmd/nerdctl/compose/compose_restart.go index 7f1e3e633e1..5974a78da03 100644 --- a/cmd/nerdctl/compose_restart.go +++ b/cmd/nerdctl/compose/compose_restart.go @@ -14,9 +14,10 @@ limitations under the License. */ -package main +package compose import ( + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" "github.com/containerd/nerdctl/pkg/composer" "github.com/spf13/cobra" ) @@ -44,7 +45,7 @@ func composeRestartAction(cmd *cobra.Command, args []string) error { opt.Timeout = &timeValue } - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } diff --git a/cmd/nerdctl/compose_rm.go b/cmd/nerdctl/compose/compose_rm.go similarity index 94% rename from cmd/nerdctl/compose_rm.go rename to cmd/nerdctl/compose/compose_rm.go index 1f718b29079..0ecb05277b1 100644 --- a/cmd/nerdctl/compose_rm.go +++ b/cmd/nerdctl/compose/compose_rm.go @@ -14,12 +14,13 @@ limitations under the License. */ -package main +package compose import ( "fmt" "strings" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" "github.com/containerd/nerdctl/pkg/composer" "github.com/spf13/cobra" ) @@ -68,7 +69,7 @@ func composeRemoveAction(cmd *cobra.Command, args []string) error { return err } - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } diff --git a/cmd/nerdctl/compose_run.go b/cmd/nerdctl/compose/compose_run.go similarity index 98% rename from cmd/nerdctl/compose_run.go rename to cmd/nerdctl/compose/compose_run.go index 43413b7b69d..3548ccd062f 100644 --- a/cmd/nerdctl/compose_run.go +++ b/cmd/nerdctl/compose/compose_run.go @@ -14,12 +14,13 @@ limitations under the License. */ -package main +package compose import ( "errors" "fmt" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" "github.com/containerd/nerdctl/pkg/composer" "github.com/spf13/cobra" ) @@ -167,7 +168,7 @@ func composeRunAction(cmd *cobra.Command, args []string) error { return errors.New("currently flag -t and -d cannot be specified together (FIXME)") } - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } diff --git a/cmd/nerdctl/compose_stop.go b/cmd/nerdctl/compose/compose_stop.go similarity index 92% rename from cmd/nerdctl/compose_stop.go rename to cmd/nerdctl/compose/compose_stop.go index 09d2a1943c8..e0e9d784e2d 100644 --- a/cmd/nerdctl/compose_stop.go +++ b/cmd/nerdctl/compose/compose_stop.go @@ -14,9 +14,10 @@ limitations under the License. */ -package main +package compose import ( + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" "github.com/containerd/nerdctl/pkg/composer" "github.com/spf13/cobra" ) @@ -44,7 +45,7 @@ func composeStopAction(cmd *cobra.Command, args []string) error { opt.Timeout = &timeValue } - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } diff --git a/cmd/nerdctl/compose_top.go b/cmd/nerdctl/compose/compose_top.go similarity index 88% rename from cmd/nerdctl/compose_top.go rename to cmd/nerdctl/compose/compose_top.go index e0b9d6fd4be..fb418e764f8 100644 --- a/cmd/nerdctl/compose_top.go +++ b/cmd/nerdctl/compose/compose_top.go @@ -14,12 +14,14 @@ limitations under the License. */ -package main +package compose import ( "fmt" "github.com/containerd/containerd" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/action" "github.com/containerd/nerdctl/pkg/labels" "github.com/spf13/cobra" ) @@ -37,7 +39,7 @@ func newComposeTopCommand() *cobra.Command { } func composeTopAction(cmd *cobra.Command, args []string) error { - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } @@ -72,7 +74,7 @@ func composeTopAction(cmd *cobra.Command, args []string) error { } fmt.Fprintf(stdout, "%s\n", info.Labels[labels.Name]) // `compose ps` uses empty ps args - err = containerTop(ctx, cmd, client, c.ID(), "") + err = action.ContainerTop(ctx, cmd, client, c.ID(), "") if err != nil { return err } diff --git a/cmd/nerdctl/compose_up.go b/cmd/nerdctl/compose/compose_up.go similarity index 96% rename from cmd/nerdctl/compose_up.go rename to cmd/nerdctl/compose/compose_up.go index 8863711e2a7..53d59401098 100644 --- a/cmd/nerdctl/compose_up.go +++ b/cmd/nerdctl/compose/compose_up.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package compose import ( "errors" @@ -22,6 +22,7 @@ import ( "strconv" "strings" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" "github.com/containerd/nerdctl/pkg/composer" "github.com/spf13/cobra" ) @@ -99,7 +100,7 @@ func composeUpAction(cmd *cobra.Command, services []string) error { scale[parts[0]] = uint64(replicas) } - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } diff --git a/cmd/nerdctl/compose_version.go b/cmd/nerdctl/compose/compose_version.go similarity index 99% rename from cmd/nerdctl/compose_version.go rename to cmd/nerdctl/compose/compose_version.go index dbea1318ec6..48316a3d117 100644 --- a/cmd/nerdctl/compose_version.go +++ b/cmd/nerdctl/compose/compose_version.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package compose import ( "fmt" diff --git a/cmd/nerdctl/commit.go b/cmd/nerdctl/container/commit.go similarity index 92% rename from cmd/nerdctl/commit.go rename to cmd/nerdctl/container/commit.go index 7db1853edb2..404fe67d003 100644 --- a/cmd/nerdctl/commit.go +++ b/cmd/nerdctl/container/commit.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package container import ( "context" @@ -22,6 +22,9 @@ import ( "fmt" "strings" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/cmd/nerdctl/utils" "github.com/containerd/nerdctl/pkg/idutil/containerwalker" "github.com/containerd/nerdctl/pkg/imgutil/commit" "github.com/containerd/nerdctl/pkg/referenceutil" @@ -29,11 +32,11 @@ import ( "github.com/spf13/cobra" ) -func newCommitCommand() *cobra.Command { +func NewCommitCommand() *cobra.Command { var commitCommand = &cobra.Command{ Use: "commit [flags] CONTAINER REPOSITORY[:TAG]", Short: "Create a new image from a container's changes", - Args: IsExactArgs(2), + Args: utils.IsExactArgs(2), RunE: commitAction, ValidArgsFunction: commitShellComplete, SilenceUsage: true, @@ -52,7 +55,7 @@ func commitAction(cmd *cobra.Command, args []string) error { return err } - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } @@ -162,7 +165,7 @@ func newCommitOpts(cmd *cobra.Command, args []string) (*commit.Opts, error) { func commitShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(args) == 0 { - return shellCompleteContainerNames(cmd, nil) + return completion.ShellCompleteContainerNames(cmd, nil) } return nil, cobra.ShellCompDirectiveNoFileComp } diff --git a/cmd/nerdctl/container.go b/cmd/nerdctl/container/container.go similarity index 52% rename from cmd/nerdctl/container.go rename to cmd/nerdctl/container/container.go index 6c0a763f14b..88aecdde027 100644 --- a/cmd/nerdctl/container.go +++ b/cmd/nerdctl/container/container.go @@ -14,48 +14,52 @@ limitations under the License. */ -package main +package container import ( + "github.com/containerd/nerdctl/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/common" "github.com/spf13/cobra" ) -func newContainerCommand() *cobra.Command { +func NewContainerCommand() *cobra.Command { containerCommand := &cobra.Command{ - Annotations: map[string]string{Category: Management}, + Annotations: map[string]string{common.Category: common.Management}, Use: "container", Short: "Manage containers", - RunE: unknownSubcommandAction, + RunE: completion.UnknownSubcommandAction, SilenceUsage: true, SilenceErrors: true, } containerCommand.AddCommand( - newCreateCommand(), - newRunCommand(), - newUpdateCommand(), - newExecCommand(), - containerLsCommand(), - newContainerInspectCommand(), - newLogsCommand(), - newPortCommand(), - newRmCommand(), - newStopCommand(), - newStartCommand(), - newRestartCommand(), - newKillCommand(), - newPauseCommand(), - newWaitCommand(), - newUnpauseCommand(), - newCommitCommand(), - newRenameCommand(), - newContainerPruneCommand(), + NewCreateCommand(), + NewRunCommand(), + NewUpdateCommand(), + NewExecCommand(), + NewLsCommand(), + NewInspectCommand(), + NewLogsCommand(), + NewPortCommand(), + NewRmCommand(), + NewStopCommand(), + NewStartCommand(), + NewRestartCommand(), + NewKillCommand(), + NewPauseCommand(), + NewWaitCommand(), + NewUnpauseCommand(), + NewCommitCommand(), + NewRenameCommand(), + NewPruneCommand(), + NewTopCommand(), + NewStatsCommand(), ) - addCpCommand(containerCommand) + AddCpCommand(containerCommand) return containerCommand } -func containerLsCommand() *cobra.Command { - x := newPsCommand() +func NewLsCommand() *cobra.Command { + x := NewPsCommandForMain() x.Use = "ls" x.Aliases = []string{"list"} return x diff --git a/cmd/nerdctl/container_inspect.go b/cmd/nerdctl/container/container_inspect.go similarity index 87% rename from cmd/nerdctl/container_inspect.go rename to cmd/nerdctl/container/container_inspect.go index 8580af92130..3a22e96bd56 100644 --- a/cmd/nerdctl/container_inspect.go +++ b/cmd/nerdctl/container/container_inspect.go @@ -14,13 +14,16 @@ limitations under the License. */ -package main +package container import ( "context" "fmt" "time" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/fmtutil" "github.com/containerd/nerdctl/pkg/containerinspector" "github.com/containerd/nerdctl/pkg/idutil/containerwalker" "github.com/containerd/nerdctl/pkg/inspecttypes/dockercompat" @@ -28,13 +31,13 @@ import ( "github.com/spf13/cobra" ) -func newContainerInspectCommand() *cobra.Command { +func NewInspectCommand() *cobra.Command { var containerInspectCommand = &cobra.Command{ Use: "inspect [flags] CONTAINER [CONTAINER, ...]", Short: "Display detailed information on one or more containers.", Long: "Hint: set `--mode=native` for showing the full output", Args: cobra.MinimumNArgs(1), - RunE: containerInspectAction, + RunE: InspectAction, ValidArgsFunction: containerInspectShellComplete, SilenceUsage: true, SilenceErrors: true, @@ -50,8 +53,8 @@ func newContainerInspectCommand() *cobra.Command { return containerInspectCommand } -func containerInspectAction(cmd *cobra.Command, args []string) error { - client, ctx, cancel, err := newClient(cmd) +func InspectAction(cmd *cobra.Command, args []string) error { + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } @@ -82,7 +85,7 @@ func containerInspectAction(cmd *cobra.Command, args []string) error { return fmt.Errorf("%d errors: %v", len(errs), errs) } - return formatSlice(cmd, f.entries) + return fmtutil.FormatSlice(cmd, f.entries) } type containerInspector struct { @@ -115,5 +118,5 @@ func (x *containerInspector) Handler(ctx context.Context, found containerwalker. func containerInspectShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // show container names - return shellCompleteContainerNames(cmd, nil) + return completion.ShellCompleteContainerNames(cmd, nil) } diff --git a/cmd/nerdctl/container_prune.go b/cmd/nerdctl/container/container_prune.go similarity index 80% rename from cmd/nerdctl/container_prune.go rename to cmd/nerdctl/container/container_prune.go index ab38ad8d1f2..cd90352d42a 100644 --- a/cmd/nerdctl/container_prune.go +++ b/cmd/nerdctl/container/container_prune.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package container import ( "context" @@ -23,11 +23,15 @@ import ( "strings" "github.com/containerd/containerd" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/action" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/common" + "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) -func newContainerPruneCommand() *cobra.Command { +func NewPruneCommand() *cobra.Command { containerPruneCommand := &cobra.Command{ Use: "prune [flags]", Short: "Remove all stopped containers", @@ -58,16 +62,16 @@ func containerPruneAction(cmd *cobra.Command, _ []string) error { } } - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } defer cancel() - return containerPrune(ctx, cmd, client) + return Prune(ctx, cmd, client) } -func containerPrune(ctx context.Context, cmd *cobra.Command, client *containerd.Client) error { +func Prune(ctx context.Context, cmd *cobra.Command, client *containerd.Client) error { containers, err := client.Containers(ctx) if err != nil { return err @@ -75,12 +79,12 @@ func containerPrune(ctx context.Context, cmd *cobra.Command, client *containerd. var deleted []string for _, container := range containers { - err = removeContainer(ctx, cmd, container, false, true) + err = action.RemoveContainer(ctx, cmd, container, false, true) if err == nil { deleted = append(deleted, container.ID()) continue } - if errors.As(err, &statusError{}) { + if errors.As(err, &common.StatusError{}) { continue } logrus.WithError(err).Warnf("failed to remove container %s", container.ID()) diff --git a/cmd/nerdctl/cp.go b/cmd/nerdctl/container/cp.go similarity index 98% rename from cmd/nerdctl/cp.go rename to cmd/nerdctl/container/cp.go index a80135b16bd..716fc8fc5c9 100644 --- a/cmd/nerdctl/cp.go +++ b/cmd/nerdctl/container/cp.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package container import ( "errors" diff --git a/cmd/nerdctl/container/cp_freebsd.go b/cmd/nerdctl/container/cp_freebsd.go new file mode 100644 index 00000000000..4e7d2cfd518 --- /dev/null +++ b/cmd/nerdctl/container/cp_freebsd.go @@ -0,0 +1,23 @@ +/* + 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 container + +import "github.com/spf13/cobra" + +func AddCpCommand(rootCmd *cobra.Command) { + // NOP +} diff --git a/cmd/nerdctl/cp_linux.go b/cmd/nerdctl/container/cp_linux.go similarity index 96% rename from cmd/nerdctl/cp_linux.go rename to cmd/nerdctl/container/cp_linux.go index 1df784636f7..54718ef5fef 100644 --- a/cmd/nerdctl/cp_linux.go +++ b/cmd/nerdctl/container/cp_linux.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package container import ( "context" @@ -27,6 +27,7 @@ import ( "strings" "github.com/containerd/containerd" + "github.com/containerd/nerdctl/cmd/nerdctl/utils" "github.com/containerd/nerdctl/pkg/inspecttypes/native" "github.com/containerd/nerdctl/pkg/rootlessutil" "github.com/containerd/nerdctl/pkg/tarutil" @@ -35,7 +36,7 @@ import ( "github.com/spf13/cobra" ) -func newCpCommand() *cobra.Command { +func NewCpCommand() *cobra.Command { shortHelp := "Copy files/folders between a running container and the local filesystem." @@ -52,7 +53,7 @@ Using 'nerdctl cp' with untrusted or malicious containers is unsupported and may nerdctl cp [flags] SRC_PATH|- CONTAINER:DEST_PATH` var cpCommand = &cobra.Command{ Use: usage, - Args: IsExactArgs(2), + Args: utils.IsExactArgs(2), Short: shortHelp, Long: longHelp, RunE: cpAction, @@ -105,7 +106,7 @@ func cpAction(cmd *cobra.Command, args []string) error { ctx := cmd.Context() // cp works in the host namespace (for inspecting file permissions), so we can't directly use the Go client. - selfExe, inspectArgs := globalFlags(cmd) + selfExe, inspectArgs := utils.GlobalFlags(cmd) inspectArgs = append(inspectArgs, "container", "inspect", "--mode=native", "--format={{json .Process}}", container) inspectCmd := exec.CommandContext(ctx, selfExe, inspectArgs...) inspectCmd.Stderr = os.Stderr @@ -272,3 +273,7 @@ func kopy(ctx context.Context, container2host bool, pid int, dst, src string, fo } return nil } + +func AddCpCommand(rootCmd *cobra.Command) { + rootCmd.AddCommand(NewCpCommand()) +} diff --git a/cmd/nerdctl/container/cp_windows.go b/cmd/nerdctl/container/cp_windows.go new file mode 100644 index 00000000000..4e7d2cfd518 --- /dev/null +++ b/cmd/nerdctl/container/cp_windows.go @@ -0,0 +1,23 @@ +/* + 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 container + +import "github.com/spf13/cobra" + +func AddCpCommand(rootCmd *cobra.Command) { + // NOP +} diff --git a/cmd/nerdctl/create.go b/cmd/nerdctl/container/create.go similarity index 81% rename from cmd/nerdctl/create.go rename to cmd/nerdctl/container/create.go index b0811f17da7..3c55f6b086d 100644 --- a/cmd/nerdctl/create.go +++ b/cmd/nerdctl/container/create.go @@ -14,16 +14,19 @@ limitations under the License. */ -package main +package container import ( "fmt" "runtime" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/action/run" "github.com/spf13/cobra" ) -func newCreateCommand() *cobra.Command { +func NewCreateCommand() *cobra.Command { shortHelp := "Create a new container. Optionally specify \"ipfs://\" or \"ipns://\" scheme to pull image from IPFS." longHelp := shortHelp switch runtime.GOOS { @@ -40,12 +43,12 @@ func newCreateCommand() *cobra.Command { Short: shortHelp, Long: longHelp, RunE: createAction, - ValidArgsFunction: runShellComplete, + ValidArgsFunction: completion.RunShellComplete, SilenceUsage: true, SilenceErrors: true, } createCommand.Flags().SetInterspersed(false) - setCreateFlags(createCommand) + run.SetCreateFlags(createCommand) return createCommand } @@ -64,13 +67,13 @@ func createAction(cmd *cobra.Command, args []string) error { return fmt.Errorf("%s requires experimental mode to be enabled", platform) } - client, ctx, cancel, err := newClientWithPlatform(cmd, platform) + client, ctx, cancel, err := ncclient.NewWithPlatform(cmd, platform) if err != nil { return err } defer cancel() - container, gc, err := createContainer(ctx, cmd, client, args, platform, false, false, true) + container, gc, err := run.CreateContainer(ctx, cmd, client, args, platform, false, false, true) if err != nil { if gc != nil { gc() diff --git a/cmd/nerdctl/exec.go b/cmd/nerdctl/container/exec.go similarity index 94% rename from cmd/nerdctl/exec.go rename to cmd/nerdctl/container/exec.go index 5920e4a69b7..f7d75ee0685 100644 --- a/cmd/nerdctl/exec.go +++ b/cmd/nerdctl/container/exec.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package container import ( "context" @@ -28,6 +28,9 @@ import ( "github.com/containerd/containerd/cio" "github.com/containerd/containerd/cmd/ctr/commands" "github.com/containerd/containerd/cmd/ctr/commands/tasks" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/action/run" "github.com/containerd/nerdctl/pkg/flagutil" "github.com/containerd/nerdctl/pkg/idgen" "github.com/containerd/nerdctl/pkg/idutil/containerwalker" @@ -38,7 +41,7 @@ import ( "github.com/spf13/cobra" ) -func newExecCommand() *cobra.Command { +func NewExecCommand() *cobra.Command { var execCommand = &cobra.Command{ Use: "exec [flags] CONTAINER COMMAND [ARG...]", Args: cobra.MinimumNArgs(2), @@ -72,7 +75,7 @@ func execAction(cmd *cobra.Command, args []string) error { args = newArg } - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } @@ -208,7 +211,7 @@ func generateExecProcessSpec(ctx context.Context, cmd *cobra.Command, args []str if err != nil { return nil, err } - userOpts, err := generateUserOpts(cmd) + userOpts, err := run.GenerateUserOpts(cmd) if err != nil { return nil, err } @@ -247,7 +250,7 @@ func generateExecProcessSpec(ctx context.Context, cmd *cobra.Command, args []str if err != nil { return nil, err } - envs, err := generateEnvs(envFile, env) + envs, err := run.GenerateEnvs(envFile, env) if err != nil { return nil, err } @@ -273,7 +276,7 @@ func execShellComplete(cmd *cobra.Command, args []string, toComplete string) ([] statusFilterFn := func(st containerd.ProcessStatus) bool { return st == containerd.Running } - return shellCompleteContainerNames(cmd, statusFilterFn) + return completion.ShellCompleteContainerNames(cmd, statusFilterFn) } return nil, cobra.ShellCompDirectiveNoFileComp } diff --git a/cmd/nerdctl/exec_freebsd.go b/cmd/nerdctl/container/exec_freebsd.go similarity index 97% rename from cmd/nerdctl/exec_freebsd.go rename to cmd/nerdctl/container/exec_freebsd.go index 8d374c3b9c2..57ebca59e4a 100644 --- a/cmd/nerdctl/exec_freebsd.go +++ b/cmd/nerdctl/container/exec_freebsd.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package container import ( "github.com/opencontainers/runtime-spec/specs-go" diff --git a/cmd/nerdctl/exec_linux.go b/cmd/nerdctl/container/exec_linux.go similarity index 98% rename from cmd/nerdctl/exec_linux.go rename to cmd/nerdctl/container/exec_linux.go index 30c9cfa9eb9..28fb7c09028 100644 --- a/cmd/nerdctl/exec_linux.go +++ b/cmd/nerdctl/container/exec_linux.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package container import ( "github.com/containerd/containerd/pkg/cap" diff --git a/cmd/nerdctl/exec_windows.go b/cmd/nerdctl/container/exec_windows.go similarity index 97% rename from cmd/nerdctl/exec_windows.go rename to cmd/nerdctl/container/exec_windows.go index 88ed1bcc369..0e8eb6bc563 100644 --- a/cmd/nerdctl/exec_windows.go +++ b/cmd/nerdctl/container/exec_windows.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package container import ( "github.com/opencontainers/runtime-spec/specs-go" diff --git a/cmd/nerdctl/kill.go b/cmd/nerdctl/container/kill.go similarity index 92% rename from cmd/nerdctl/kill.go rename to cmd/nerdctl/container/kill.go index a354f00e9a6..090ff6d4004 100644 --- a/cmd/nerdctl/kill.go +++ b/cmd/nerdctl/container/kill.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package container import ( "context" @@ -26,6 +26,8 @@ import ( "github.com/containerd/containerd" "github.com/containerd/containerd/cio" "github.com/containerd/containerd/errdefs" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/completion" "github.com/containerd/nerdctl/pkg/idutil/containerwalker" "github.com/moby/sys/signal" @@ -33,7 +35,7 @@ import ( "github.com/spf13/cobra" ) -func newKillCommand() *cobra.Command { +func NewKillCommand() *cobra.Command { var killCommand = &cobra.Command{ Use: "kill [flags] CONTAINER [CONTAINER, ...]", Short: "Kill one or more running containers", @@ -61,7 +63,7 @@ func killAction(cmd *cobra.Command, args []string) error { return err } - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } @@ -134,6 +136,6 @@ func killShellComplete(cmd *cobra.Command, args []string, toComplete string) ([] statusFilterFn := func(st containerd.ProcessStatus) bool { return st != containerd.Stopped && st != containerd.Created && st != containerd.Unknown } - return shellCompleteContainerNames(cmd, statusFilterFn) + return completion.ShellCompleteContainerNames(cmd, statusFilterFn) } diff --git a/cmd/nerdctl/logs.go b/cmd/nerdctl/container/logs.go similarity index 91% rename from cmd/nerdctl/logs.go rename to cmd/nerdctl/container/logs.go index b4c5ff8b593..77338045f36 100644 --- a/cmd/nerdctl/logs.go +++ b/cmd/nerdctl/container/logs.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package container import ( "context" @@ -25,6 +25,9 @@ import ( "syscall" "github.com/containerd/containerd" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/cmd/nerdctl/utils" "github.com/containerd/nerdctl/pkg/idutil/containerwalker" "github.com/containerd/nerdctl/pkg/labels" "github.com/containerd/nerdctl/pkg/logging" @@ -32,13 +35,13 @@ import ( "github.com/spf13/cobra" ) -func newLogsCommand() *cobra.Command { +func NewLogsCommand() *cobra.Command { var logsCommand = &cobra.Command{ Use: "logs [flags] CONTAINER", - Args: IsExactArgs(1), + Args: utils.IsExactArgs(1), Short: "Fetch the logs of a container. Currently, only containers created with `nerdctl run -d` are supported.", RunE: logsAction, - ValidArgsFunction: logsShellComplete, + ValidArgsFunction: completion.LogsShellComplete, SilenceUsage: true, SilenceErrors: true, } @@ -51,7 +54,7 @@ func newLogsCommand() *cobra.Command { } func logsAction(cmd *cobra.Command, args []string) error { - dataStore, err := getDataStore(cmd) + dataStore, err := ncclient.DataStore(cmd) if err != nil { return err } @@ -65,7 +68,7 @@ func logsAction(cmd *cobra.Command, args []string) error { logrus.Warn("Currently, `nerdctl logs` only supports containers created with `nerdctl run -d`") } - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } @@ -167,11 +170,6 @@ func logsAction(cmd *cobra.Command, args []string) error { return nil } -func logsShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - // show container names (TODO: only show containers with logs) - return shellCompleteContainerNames(cmd, nil) -} - // Attempts to parse the argument given to `-n/--tail` as a uint. func getTailArgAsUint(arg string) (uint, error) { if arg == "all" { diff --git a/cmd/nerdctl/pause.go b/cmd/nerdctl/container/pause.go similarity index 59% rename from cmd/nerdctl/pause.go rename to cmd/nerdctl/container/pause.go index aaf4d5ae860..e89c37d45e4 100644 --- a/cmd/nerdctl/pause.go +++ b/cmd/nerdctl/container/pause.go @@ -14,26 +14,27 @@ limitations under the License. */ -package main +package container import ( "context" "fmt" - "github.com/containerd/containerd" - "github.com/containerd/containerd/cio" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/action" "github.com/containerd/nerdctl/pkg/idutil/containerwalker" "github.com/spf13/cobra" ) -func newPauseCommand() *cobra.Command { +func NewPauseCommand() *cobra.Command { var pauseCommand = &cobra.Command{ Use: "pause [flags] CONTAINER [CONTAINER, ...]", Args: cobra.MinimumNArgs(1), Short: "Pause all processes within one or more containers", RunE: pauseAction, - ValidArgsFunction: pauseShellComplete, + ValidArgsFunction: completion.PauseShellComplete, SilenceUsage: true, SilenceErrors: true, } @@ -41,7 +42,7 @@ func newPauseCommand() *cobra.Command { } func pauseAction(cmd *cobra.Command, args []string) error { - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } @@ -53,7 +54,7 @@ func pauseAction(cmd *cobra.Command, args []string) error { if found.MatchCount > 1 { return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req) } - if err := pauseContainer(ctx, client, found.Container.ID()); err != nil { + if err := action.PauseContainer(ctx, client, found.Container.ID()); err != nil { return err } @@ -71,37 +72,3 @@ func pauseAction(cmd *cobra.Command, args []string) error { } return nil } - -func pauseContainer(ctx context.Context, client *containerd.Client, id string) error { - container, err := client.LoadContainer(ctx, id) - if err != nil { - return err - } - - task, err := container.Task(ctx, cio.Load) - if err != nil { - return err - } - - status, err := task.Status(ctx) - if err != nil { - return err - } - - switch status.Status { - case containerd.Paused: - return fmt.Errorf("container %s is already paused", id) - case containerd.Created, containerd.Stopped: - return fmt.Errorf("container %s is not running", id) - default: - return task.Pause(ctx) - } -} - -func pauseShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - // show running container names - statusFilterFn := func(st containerd.ProcessStatus) bool { - return st == containerd.Running - } - return shellCompleteContainerNames(cmd, statusFilterFn) -} diff --git a/cmd/nerdctl/port.go b/cmd/nerdctl/container/port.go similarity index 89% rename from cmd/nerdctl/port.go rename to cmd/nerdctl/container/port.go index ef39fa14f94..a7ab4328684 100644 --- a/cmd/nerdctl/port.go +++ b/cmd/nerdctl/container/port.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package container import ( "context" @@ -22,13 +22,15 @@ import ( "strconv" "strings" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/completion" "github.com/containerd/nerdctl/pkg/containerutil" "github.com/containerd/nerdctl/pkg/idutil/containerwalker" "github.com/spf13/cobra" ) -func newPortCommand() *cobra.Command { +func NewPortCommand() *cobra.Command { var portCommand = &cobra.Command{ Use: "port [flags] CONTAINER [PRIVATE_PORT[/PROTO]]", Args: cobra.RangeArgs(1, 2), @@ -70,7 +72,7 @@ func portAction(cmd *cobra.Command, args []string) error { } } - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } @@ -96,5 +98,5 @@ func portAction(cmd *cobra.Command, args []string) error { } func portShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return shellCompleteContainerNames(cmd, nil) + return completion.ShellCompleteContainerNames(cmd, nil) } diff --git a/cmd/nerdctl/ps.go b/cmd/nerdctl/container/ps.go similarity index 76% rename from cmd/nerdctl/ps.go rename to cmd/nerdctl/container/ps.go index b377263d82b..29d68519662 100644 --- a/cmd/nerdctl/ps.go +++ b/cmd/nerdctl/container/ps.go @@ -14,12 +14,11 @@ limitations under the License. */ -package main +package container import ( "bytes" "context" - "encoding/json" "errors" "fmt" "io" @@ -34,15 +33,19 @@ import ( "github.com/containerd/containerd/containers" "github.com/containerd/containerd/errdefs" "github.com/containerd/containerd/pkg/progress" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/utils" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/containerfilter" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/fmtutil" "github.com/containerd/nerdctl/pkg/formatter" "github.com/containerd/nerdctl/pkg/labels" - "github.com/containerd/nerdctl/pkg/labels/k8slabels" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) -func newPsCommand() *cobra.Command { +// NewPsCommandForMain is a top-level subcommand. +func NewPsCommandForMain() *cobra.Command { var psCommand = &cobra.Command{ Use: "ps", Args: cobra.NoArgs, @@ -68,7 +71,7 @@ func newPsCommand() *cobra.Command { } func psAction(cmd *cobra.Command, args []string) error { - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } @@ -96,7 +99,7 @@ func psAction(cmd *cobra.Command, args []string) error { if err != nil { return err } - filterCtx, err := foldContainerFilters(ctx, containers, filters) + filterCtx, err := containerfilter.FoldContainerFilters(ctx, containers, filters) if err != nil { return err } @@ -182,7 +185,7 @@ func printContainers(ctx context.Context, client *containerd.Client, cmd *cobra. return errors.New("format and quiet must not be specified together") } var err error - tmpl, err = parseTemplate(format) + tmpl, err = fmtutil.ParseTemplate(format) if err != nil { return err } @@ -224,7 +227,7 @@ func printContainers(ctx context.Context, client *containerd.Client, cmd *cobra. ID: id, Image: imageName, Platform: info.Labels[labels.Platform], - Names: getPrintableContainerName(info.Labels), + Names: utils.PrintableContainerName(info.Labels), Ports: formatter.FormatPorts(info.Labels), Status: cStatus, Runtime: info.Runtime.Name, @@ -232,7 +235,7 @@ func printContainers(ctx context.Context, client *containerd.Client, cmd *cobra. } if size || wide { - containerSize, err := getContainerSize(ctx, client, c, info) + containerSize, err := containerSize(ctx, client, c, info) if err != nil { return err } @@ -277,82 +280,13 @@ func printContainers(ctx context.Context, client *containerd.Client, cmd *cobra. } } - if f, ok := w.(Flusher); ok { + if f, ok := w.(fmtutil.Flusher); ok { return f.Flush() } return nil } -func getPrintableContainerName(containerLabels map[string]string) string { - if name, ok := containerLabels[labels.Name]; ok { - return name - } - - if ns, ok := containerLabels[k8slabels.PodNamespace]; ok { - if podName, ok := containerLabels[k8slabels.PodName]; ok { - if containerName, ok := containerLabels[k8slabels.ContainerName]; ok { - // Container - return fmt.Sprintf("k8s://%s/%s/%s", ns, podName, containerName) - } - // Pod sandbox - return fmt.Sprintf("k8s://%s/%s", ns, podName) - } - } - return "" -} - -type containerVolume struct { - Type string - Name string - Source string - Destination string - Mode string - RW bool - Propagation string -} - -func getContainerVolumes(containerLabels map[string]string) []*containerVolume { - var vols []*containerVolume - volLabels := []string{labels.AnonymousVolumes, labels.Mounts} - for _, volLabel := range volLabels { - names, ok := containerLabels[volLabel] - if !ok { - continue - } - var ( - volumes []*containerVolume - err error - ) - if volLabel == labels.Mounts { - err = json.Unmarshal([]byte(names), &volumes) - } - if volLabel == labels.AnonymousVolumes { - var anonymous []string - err = json.Unmarshal([]byte(names), &anonymous) - for _, anony := range anonymous { - volumes = append(volumes, &containerVolume{Name: anony}) - } - - } - if err != nil { - logrus.Warn(err) - } - vols = append(vols, volumes...) - } - return vols -} - -func getContainerNetworks(containerLables map[string]string) []string { - var networks []string - if names, ok := containerLables[labels.Networks]; ok { - if err := json.Unmarshal([]byte(names), &networks); err != nil { - logrus.Warn(err) - } - } - return networks -} - -func getContainerSize(ctx context.Context, client *containerd.Client, c containerd.Container, info containers.Container) (string, error) { +func containerSize(ctx context.Context, client *containerd.Client, c containerd.Container, info containers.Container) (string, error) { // get container snapshot size snapshotKey := info.SnapshotKey var containerSize int64 @@ -373,7 +307,7 @@ func getContainerSize(ctx context.Context, client *containerd.Client, c containe sn := client.SnapshotService(info.Snapshotter) - imageSize, err := unpackedImageSize(ctx, sn, image) + imageSize, err := utils.UnpackedImageSize(ctx, sn, image) if err != nil { return "", err } diff --git a/cmd/nerdctl/rename.go b/cmd/nerdctl/container/rename.go similarity index 86% rename from cmd/nerdctl/rename.go rename to cmd/nerdctl/container/rename.go index 907af2d55ab..d90af5fe7cb 100644 --- a/cmd/nerdctl/rename.go +++ b/cmd/nerdctl/container/rename.go @@ -14,13 +14,16 @@ limitations under the License. */ -package main +package container import ( "context" "fmt" "github.com/containerd/containerd" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/cmd/nerdctl/utils" "github.com/containerd/nerdctl/pkg/dnsutil/hostsstore" "github.com/containerd/nerdctl/pkg/idutil/containerwalker" "github.com/containerd/nerdctl/pkg/labels" @@ -28,10 +31,10 @@ import ( "github.com/spf13/cobra" ) -func newRenameCommand() *cobra.Command { +func NewRenameCommand() *cobra.Command { var renameCommand = &cobra.Command{ Use: "rename [flags] CONTAINER NEW_NAME", - Args: IsExactArgs(2), + Args: utils.IsExactArgs(2), Short: "rename a container", RunE: renameAction, ValidArgsFunction: renameShellComplete, @@ -42,7 +45,7 @@ func newRenameCommand() *cobra.Command { } func renameAction(cmd *cobra.Command, args []string) error { - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } @@ -52,7 +55,7 @@ func renameAction(cmd *cobra.Command, args []string) error { return err } - dataStore, err := getDataStore(cmd) + dataStore, err := ncclient.DataStore(cmd) if err != nil { return err } @@ -105,5 +108,5 @@ func renameContainer(ctx context.Context, container containerd.Container, newNam } func renameShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return shellCompleteContainerNames(cmd, nil) + return completion.ShellCompleteContainerNames(cmd, nil) } diff --git a/cmd/nerdctl/restart.go b/cmd/nerdctl/container/restart.go similarity index 81% rename from cmd/nerdctl/restart.go rename to cmd/nerdctl/container/restart.go index b5c4dd3354e..c351a06a587 100644 --- a/cmd/nerdctl/restart.go +++ b/cmd/nerdctl/container/restart.go @@ -14,24 +14,27 @@ limitations under the License. */ -package main +package container import ( "context" "fmt" "time" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/action" "github.com/containerd/nerdctl/pkg/idutil/containerwalker" "github.com/spf13/cobra" ) -func newRestartCommand() *cobra.Command { +func NewRestartCommand() *cobra.Command { var restartCommand = &cobra.Command{ Use: "restart [flags] CONTAINER [CONTAINER, ...]", Args: cobra.MinimumNArgs(1), Short: "Restart one or more running containers", RunE: restartAction, - ValidArgsFunction: startShellComplete, + ValidArgsFunction: completion.StartShellComplete, SilenceUsage: true, SilenceErrors: true, } @@ -51,7 +54,7 @@ func restartAction(cmd *cobra.Command, args []string) error { timeout = &t } - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } @@ -63,10 +66,10 @@ func restartAction(cmd *cobra.Command, args []string) error { if found.MatchCount > 1 { return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req) } - if err := stopContainer(ctx, found.Container, timeout); err != nil { + if err := action.StopContainer(ctx, found.Container, timeout); err != nil { return err } - if err := startContainer(ctx, found.Container, false, client); err != nil { + if err := action.StartContainer(ctx, found.Container, false, client); err != nil { return err } _, err = fmt.Fprintf(cmd.OutOrStdout(), "%s\n", found.Req) diff --git a/cmd/nerdctl/container/rm.go b/cmd/nerdctl/container/rm.go new file mode 100644 index 00000000000..47ee9974ccc --- /dev/null +++ b/cmd/nerdctl/container/rm.go @@ -0,0 +1,94 @@ +/* + 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 container + +import ( + "context" + "fmt" + + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/action" + "github.com/containerd/nerdctl/pkg/idutil/containerwalker" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +func NewRmCommand() *cobra.Command { + var rmCommand = &cobra.Command{ + Use: "rm [flags] CONTAINER [CONTAINER, ...]", + Args: cobra.MinimumNArgs(1), + Short: "Remove one or more containers", + RunE: rmAction, + ValidArgsFunction: rmShellComplete, + SilenceUsage: true, + SilenceErrors: true, + } + rmCommand.Flags().BoolP("force", "f", false, "Force the removal of a running|paused|unknown container (uses SIGKILL)") + rmCommand.Flags().BoolP("volumes", "v", false, "Remove volume associated with the container") + return rmCommand +} + +func rmAction(cmd *cobra.Command, args []string) error { + force, err := cmd.Flags().GetBool("force") + if err != nil { + return err + } + removeAnonVolumes, err := cmd.Flags().GetBool("volumes") + if err != nil { + return err + } + + client, ctx, cancel, err := ncclient.New(cmd) + if err != nil { + return err + } + defer cancel() + + walker := &containerwalker.ContainerWalker{ + Client: client, + OnFound: func(ctx context.Context, found containerwalker.Found) error { + if found.MatchCount > 1 { + return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req) + } + if err := action.RemoveContainer(ctx, cmd, found.Container, force, removeAnonVolumes); err != nil { + return err + } + _, err = fmt.Fprintf(cmd.OutOrStdout(), "%s\n", found.Req) + return err + }, + } + for _, req := range args { + n, err := walker.Walk(ctx, req) + if err == nil && n == 0 { + err = fmt.Errorf("no such container %s", req) + } + if err != nil { + if force { + logrus.Error(err) + } else { + return err + } + } + } + return nil +} + +func rmShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + // show container names + return completion.ShellCompleteContainerNames(cmd, nil) +} diff --git a/cmd/nerdctl/container/run.go b/cmd/nerdctl/container/run.go new file mode 100644 index 00000000000..5ecd646e542 --- /dev/null +++ b/cmd/nerdctl/container/run.go @@ -0,0 +1,179 @@ +/* + 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 container + +import ( + "errors" + "fmt" + "runtime" + + "github.com/containerd/console" + "github.com/containerd/containerd" + "github.com/containerd/containerd/cmd/ctr/commands" + "github.com/containerd/containerd/cmd/ctr/commands/tasks" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/action" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/action/run" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/common" + "github.com/containerd/nerdctl/pkg/labels" + "github.com/containerd/nerdctl/pkg/taskutil" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +func NewRunCommand() *cobra.Command { + shortHelp := "Run a command in a new container. Optionally specify \"ipfs://\" or \"ipns://\" scheme to pull image from IPFS." + longHelp := shortHelp + switch runtime.GOOS { + case "windows": + longHelp += "\n" + longHelp += "WARNING: `nerdctl run` is experimental on Windows and currently broken (https://github.com/containerd/nerdctl/issues/28)" + case "freebsd": + longHelp += "\n" + longHelp += "WARNING: `nerdctl run` is experimental on FreeBSD and currently requires `--net=none` (https://github.com/containerd/nerdctl/blob/main/docs/freebsd.md)" + } + var runCommand = &cobra.Command{ + Use: "run [flags] IMAGE [COMMAND] [ARG...]", + Args: cobra.MinimumNArgs(1), + Short: shortHelp, + Long: longHelp, + RunE: runAction, + ValidArgsFunction: completion.RunShellComplete, + SilenceUsage: true, + SilenceErrors: true, + } + + runCommand.Flags().SetInterspersed(false) + run.SetCreateFlags(runCommand) + + runCommand.Flags().BoolP("detach", "d", false, "Run container in background and print container ID") + + return runCommand +} + +// runAction is heavily based on ctr implementation: +// https://github.com/containerd/containerd/blob/v1.4.3/cmd/ctr/commands/run/run.go +func runAction(cmd *cobra.Command, args []string) error { + platform, err := cmd.Flags().GetString("platform") + if err != nil { + return err + } + client, ctx, cancel, err := ncclient.NewWithPlatform(cmd, platform) + if err != nil { + return err + } + defer cancel() + + flagD, err := cmd.Flags().GetBool("detach") + if err != nil { + return err + } + flagI, err := cmd.Flags().GetBool("interactive") + if err != nil { + return err + } + flagT, err := cmd.Flags().GetBool("tty") + if err != nil { + return err + } + container, gc, err := run.CreateContainer(ctx, cmd, client, args, platform, flagI, flagT, flagD) + if err != nil { + if gc != nil { + defer gc() + } + return err + } + + id := container.ID() + rm, err := cmd.Flags().GetBool("rm") + if err != nil { + return err + } + if rm { + if flagD { + return errors.New("flag -d and --rm cannot be specified together") + } + defer func() { + if err := action.RemoveContainer(ctx, cmd, container, true, true); err != nil { + logrus.WithError(err).Warnf("failed to remove container %s", id) + } + }() + } + + var con console.Console + if flagT { + con = console.Current() + defer con.Reset() + if err := con.SetRaw(); err != nil { + return err + } + } + + lab, err := container.Labels(ctx) + if err != nil { + return err + } + logURI := lab[labels.LogURI] + + task, err := taskutil.NewTask(ctx, client, container, flagI, flagT, flagD, con, logURI) + if err != nil { + return err + } + var statusC <-chan containerd.ExitStatus + if !flagD { + defer func() { + if rm { + if _, taskDeleteErr := task.Delete(ctx); taskDeleteErr != nil { + logrus.Error(taskDeleteErr) + } + } + }() + statusC, err = task.Wait(ctx) + if err != nil { + return err + } + } + + if err := task.Start(ctx); err != nil { + return err + } + + if flagD { + fmt.Fprintf(cmd.OutOrStdout(), "%s\n", id) + return nil + } + if flagT { + if err := tasks.HandleConsoleResize(ctx, task, con); err != nil { + logrus.WithError(err).Error("console resize") + } + } else { + sigc := commands.ForwardAllSignals(ctx, task) + defer commands.StopCatch(sigc) + } + status := <-statusC + code, _, err := status.Result() + if err != nil { + return err + } + if code != 0 { + return common.ExitCodeError{ + Code: int(code), + } + } + return nil +} diff --git a/cmd/nerdctl/container/start.go b/cmd/nerdctl/container/start.go new file mode 100644 index 00000000000..64de04291df --- /dev/null +++ b/cmd/nerdctl/container/start.go @@ -0,0 +1,90 @@ +/* + 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 container + +import ( + "context" + "fmt" + + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/action" + "github.com/containerd/nerdctl/pkg/idutil/containerwalker" + "github.com/spf13/cobra" +) + +func NewStartCommand() *cobra.Command { + var startCommand = &cobra.Command{ + Use: "start [flags] CONTAINER [CONTAINER, ...]", + Args: cobra.MinimumNArgs(1), + Short: "start one or more running containers", + RunE: startAction, + ValidArgsFunction: completion.StartShellComplete, + SilenceUsage: true, + SilenceErrors: true, + } + + startCommand.Flags().SetInterspersed(false) + startCommand.Flags().BoolP("attach", "a", false, "Attach STDOUT/STDERR and forward signals") + + return startCommand +} + +func startAction(cmd *cobra.Command, args []string) error { + client, ctx, cancel, err := ncclient.New(cmd) + if err != nil { + return err + } + defer cancel() + + flagA, err := cmd.Flags().GetBool("attach") + if err != nil { + return err + } + + if flagA && len(args) > 1 { + return fmt.Errorf("you cannot start and attach multiple containers at once") + } + + walker := &containerwalker.ContainerWalker{ + Client: client, + OnFound: func(ctx context.Context, found containerwalker.Found) error { + if found.MatchCount > 1 { + return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req) + } + if err := action.StartContainer(ctx, found.Container, flagA, client); err != nil { + return err + } + if !flagA { + _, err := fmt.Fprintf(cmd.OutOrStdout(), "%s\n", found.Req) + if err != nil { + return err + } + } + return err + }, + } + for _, req := range args { + n, err := walker.Walk(ctx, req) + if err != nil { + return err + } else if n == 0 { + return fmt.Errorf("no such container %s", req) + } + } + return nil +} diff --git a/cmd/nerdctl/stats.go b/cmd/nerdctl/container/stats.go similarity index 95% rename from cmd/nerdctl/stats.go rename to cmd/nerdctl/container/stats.go index e843aefc3b7..21a549f3434 100644 --- a/cmd/nerdctl/stats.go +++ b/cmd/nerdctl/container/stats.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package container import ( "bytes" @@ -27,10 +27,12 @@ import ( "text/template" "time" - "github.com/containerd/containerd" eventstypes "github.com/containerd/containerd/api/events" "github.com/containerd/containerd/errdefs" "github.com/containerd/containerd/events" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/fmtutil" "github.com/containerd/nerdctl/pkg/containerinspector" "github.com/containerd/nerdctl/pkg/eventutil" "github.com/containerd/nerdctl/pkg/formatter" @@ -44,12 +46,12 @@ import ( "github.com/spf13/cobra" ) -func newStatsCommand() *cobra.Command { +func NewStatsCommand() *cobra.Command { var statsCommand = &cobra.Command{ Use: "stats", Short: "Display a live stream of container(s) resource usage statistics.", RunE: statsAction, - ValidArgsFunction: statsShellComplete, + ValidArgsFunction: completion.StatsShellComplete, SilenceUsage: true, SilenceErrors: true, } @@ -134,7 +136,7 @@ func statsAction(cmd *cobra.Command, args []string) error { case "raw": return errors.New("unsupported format: \"raw\"") default: - tmpl, err = parseTemplate(format) + tmpl, err = fmtutil.ParseTemplate(format) if err != nil { return err } @@ -149,7 +151,7 @@ func statsAction(cmd *cobra.Command, args []string) error { waitFirst := &sync.WaitGroup{} cStats := stats{} - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } @@ -348,7 +350,7 @@ func statsAction(cmd *cobra.Command, args []string) error { } } } - if f, ok := w.(Flusher); ok { + if f, ok := w.(fmtutil.Flusher); ok { f.Flush() } @@ -390,7 +392,7 @@ func collect(cmd *cobra.Command, s *statsutil.Stats, waitFirst *sync.WaitGroup, } }() - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { s.SetError(err) return @@ -482,11 +484,3 @@ func collect(cmd *cobra.Command, s *statsutil.Stats, waitFirst *sync.WaitGroup, } } } - -func statsShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - // show running container names - statusFilterFn := func(st containerd.ProcessStatus) bool { - return st == containerd.Running - } - return shellCompleteContainerNames(cmd, statusFilterFn) -} diff --git a/cmd/nerdctl/stats_freebsd.go b/cmd/nerdctl/container/stats_freebsd.go similarity index 98% rename from cmd/nerdctl/stats_freebsd.go rename to cmd/nerdctl/container/stats_freebsd.go index a6460b3218a..e2dbea58ea6 100644 --- a/cmd/nerdctl/stats_freebsd.go +++ b/cmd/nerdctl/container/stats_freebsd.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package container import ( "github.com/containerd/nerdctl/pkg/inspecttypes/native" diff --git a/cmd/nerdctl/stats_linux.go b/cmd/nerdctl/container/stats_linux.go similarity index 99% rename from cmd/nerdctl/stats_linux.go rename to cmd/nerdctl/container/stats_linux.go index 8f6ddbbee36..e695b515a56 100644 --- a/cmd/nerdctl/stats_linux.go +++ b/cmd/nerdctl/container/stats_linux.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package container import ( "errors" diff --git a/cmd/nerdctl/stats_windows.go b/cmd/nerdctl/container/stats_windows.go similarity index 98% rename from cmd/nerdctl/stats_windows.go rename to cmd/nerdctl/container/stats_windows.go index a6460b3218a..e2dbea58ea6 100644 --- a/cmd/nerdctl/stats_windows.go +++ b/cmd/nerdctl/container/stats_windows.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package container import ( "github.com/containerd/nerdctl/pkg/inspecttypes/native" diff --git a/cmd/nerdctl/container/stop.go b/cmd/nerdctl/container/stop.go new file mode 100644 index 00000000000..a0d10ab213b --- /dev/null +++ b/cmd/nerdctl/container/stop.go @@ -0,0 +1,91 @@ +/* + 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 container + +import ( + "context" + "fmt" + "time" + + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/action" + "github.com/spf13/cobra" + + "github.com/containerd/containerd/errdefs" + "github.com/containerd/nerdctl/pkg/idutil/containerwalker" +) + +func NewStopCommand() *cobra.Command { + var stopCommand = &cobra.Command{ + Use: "stop [flags] CONTAINER [CONTAINER, ...]", + Args: cobra.MinimumNArgs(1), + Short: "Stop one or more running containers", + RunE: stopAction, + ValidArgsFunction: completion.StopShellComplete, + SilenceUsage: true, + SilenceErrors: true, + } + stopCommand.Flags().IntP("time", "t", 10, "Seconds to wait for stop before killing it") + return stopCommand +} + +func stopAction(cmd *cobra.Command, args []string) error { + // Time to wait after sending a SIGTERM and before sending a SIGKILL. + var timeout *time.Duration + if cmd.Flags().Changed("time") { + timeValue, err := cmd.Flags().GetInt("time") + if err != nil { + return err + } + t := time.Duration(timeValue) * time.Second + timeout = &t + } + + client, ctx, cancel, err := ncclient.New(cmd) + if err != nil { + return err + } + defer cancel() + + walker := &containerwalker.ContainerWalker{ + Client: client, + OnFound: func(ctx context.Context, found containerwalker.Found) error { + if found.MatchCount > 1 { + return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req) + } + if err := action.StopContainer(ctx, found.Container, timeout); err != nil { + if errdefs.IsNotFound(err) { + fmt.Fprintf(cmd.ErrOrStderr(), "No such container: %s\n", found.Req) + return nil + } + return err + } + _, err := fmt.Fprintf(cmd.OutOrStdout(), "%s\n", found.Req) + return err + }, + } + for _, req := range args { + n, err := walker.Walk(ctx, req) + if err != nil { + return err + } else if n == 0 { + return fmt.Errorf("no such container %s", req) + } + } + return nil +} diff --git a/cmd/nerdctl/container/top.go b/cmd/nerdctl/container/top.go new file mode 100644 index 00000000000..bf5642ce837 --- /dev/null +++ b/cmd/nerdctl/container/top.go @@ -0,0 +1,90 @@ +/* + 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 container + +import ( + "context" + "errors" + "fmt" + "strings" + + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/action" + "github.com/containerd/nerdctl/pkg/idutil/containerwalker" + "github.com/containerd/nerdctl/pkg/infoutil" + "github.com/containerd/nerdctl/pkg/rootlessutil" + + "github.com/spf13/cobra" +) + +func NewTopCommand() *cobra.Command { + var topCommand = &cobra.Command{ + Use: "top CONTAINER [ps OPTIONS]", + Args: cobra.MinimumNArgs(1), + Short: "Display the running processes of a container", + RunE: topAction, + ValidArgsFunction: completion.TopShellComplete, + SilenceUsage: true, + SilenceErrors: true, + } + topCommand.Flags().SetInterspersed(false) + return topCommand +} + +func topAction(cmd *cobra.Command, args []string) error { + // NOTE: rootless container does not rely on cgroupv1. + // more details about possible ways to resolve this concern: #223 + if rootlessutil.IsRootless() && infoutil.CgroupsVersion() == "1" { + return fmt.Errorf("top requires cgroup v2 for rootless containers, see https://rootlesscontaine.rs/getting-started/common/cgroup2/") + } + + cgroupManager, err := cmd.Flags().GetString("cgroup-manager") + if err != nil { + return err + } + if cgroupManager == "none" { + return errors.New("cgroup manager must not be \"none\"") + } + + client, ctx, cancel, err := ncclient.New(cmd) + if err != nil { + return err + } + defer cancel() + + walker := &containerwalker.ContainerWalker{ + Client: client, + OnFound: func(ctx context.Context, found containerwalker.Found) error { + if found.MatchCount > 1 { + return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req) + } + if err := action.ContainerTop(ctx, cmd, client, found.Container.ID(), strings.Join(args[1:], " ")); err != nil { + return err + } + return nil + }, + } + + n, err := walker.Walk(ctx, args[0]) + if err != nil { + return err + } else if n == 0 { + return fmt.Errorf("no such container %s", args[0]) + } + return nil +} diff --git a/cmd/nerdctl/unpause.go b/cmd/nerdctl/container/unpause.go similarity index 61% rename from cmd/nerdctl/unpause.go rename to cmd/nerdctl/container/unpause.go index 4af00fd80b1..926a06d5458 100644 --- a/cmd/nerdctl/unpause.go +++ b/cmd/nerdctl/container/unpause.go @@ -14,26 +14,27 @@ limitations under the License. */ -package main +package container import ( "context" "fmt" - "github.com/containerd/containerd" - "github.com/containerd/containerd/cio" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/action" "github.com/containerd/nerdctl/pkg/idutil/containerwalker" "github.com/spf13/cobra" ) -func newUnpauseCommand() *cobra.Command { +func NewUnpauseCommand() *cobra.Command { var unpauseCommand = &cobra.Command{ Use: "unpause [flags] CONTAINER [CONTAINER, ...]", Args: cobra.MinimumNArgs(1), Short: "Unpause all processes within one or more containers", RunE: unpauseAction, - ValidArgsFunction: unpauseShellComplete, + ValidArgsFunction: completion.UnpauseShellComplete, SilenceUsage: true, SilenceErrors: true, } @@ -41,7 +42,7 @@ func newUnpauseCommand() *cobra.Command { } func unpauseAction(cmd *cobra.Command, args []string) error { - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } @@ -53,7 +54,7 @@ func unpauseAction(cmd *cobra.Command, args []string) error { if found.MatchCount > 1 { return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req) } - if err := unpauseContainer(ctx, client, found.Container.ID()); err != nil { + if err := action.UnpauseContainer(ctx, client, found.Container.ID()); err != nil { return err } @@ -71,35 +72,3 @@ func unpauseAction(cmd *cobra.Command, args []string) error { } return nil } - -func unpauseContainer(ctx context.Context, client *containerd.Client, id string) error { - container, err := client.LoadContainer(ctx, id) - if err != nil { - return err - } - - task, err := container.Task(ctx, cio.Load) - if err != nil { - return err - } - - status, err := task.Status(ctx) - if err != nil { - return err - } - - switch status.Status { - case containerd.Paused: - return task.Resume(ctx) - default: - return fmt.Errorf("container %s is not paused", id) - } -} - -func unpauseShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - // show paused container names - statusFilterFn := func(st containerd.ProcessStatus) bool { - return st == containerd.Paused - } - return shellCompleteContainerNames(cmd, statusFilterFn) -} diff --git a/cmd/nerdctl/update.go b/cmd/nerdctl/container/update.go similarity index 97% rename from cmd/nerdctl/update.go rename to cmd/nerdctl/container/update.go index 1ca73dc14b2..e669a7fefeb 100644 --- a/cmd/nerdctl/update.go +++ b/cmd/nerdctl/container/update.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package container import ( "context" @@ -27,6 +27,8 @@ import ( "github.com/containerd/containerd/errdefs" "github.com/containerd/containerd/log" "github.com/containerd/containerd/pkg/cri/util" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/completion" "github.com/containerd/nerdctl/pkg/formatter" "github.com/containerd/nerdctl/pkg/idutil/containerwalker" "github.com/containerd/nerdctl/pkg/infoutil" @@ -50,7 +52,7 @@ type updateResourceOptions struct { BlkioWeight uint16 } -func newUpdateCommand() *cobra.Command { +func NewUpdateCommand() *cobra.Command { var updateCommand = &cobra.Command{ Use: "update [flags] CONTAINER [CONTAINER, ...]", Args: cobra.MinimumNArgs(1), @@ -81,7 +83,7 @@ func setUpdateFlags(cmd *cobra.Command) { } func updateAction(cmd *cobra.Command, args []string) error { - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } @@ -385,5 +387,5 @@ func copySpec(spec *runtimespec.Spec) (*runtimespec.Spec, error) { } func updateShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return shellCompleteContainerNames(cmd, nil) + return completion.ShellCompleteContainerNames(cmd, nil) } diff --git a/cmd/nerdctl/wait.go b/cmd/nerdctl/container/wait.go similarity index 90% rename from cmd/nerdctl/wait.go rename to cmd/nerdctl/container/wait.go index 64dc02ff102..7f64df4ddca 100644 --- a/cmd/nerdctl/wait.go +++ b/cmd/nerdctl/container/wait.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package container import ( "context" @@ -22,12 +22,14 @@ import ( "io" "github.com/containerd/containerd" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/completion" "github.com/containerd/nerdctl/pkg/idutil/containerwalker" "github.com/hashicorp/go-multierror" "github.com/spf13/cobra" ) -func newWaitCommand() *cobra.Command { +func NewWaitCommand() *cobra.Command { var waitCommand = &cobra.Command{ Use: "wait [flags] CONTAINER [CONTAINER, ...]", Args: cobra.MinimumNArgs(1), @@ -41,7 +43,7 @@ func newWaitCommand() *cobra.Command { } func containerWaitAction(cmd *cobra.Command, args []string) error { - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } @@ -103,5 +105,5 @@ func waitShellComplete(cmd *cobra.Command, args []string, toComplete string) ([] statusFilterFn := func(st containerd.ProcessStatus) bool { return st == containerd.Running } - return shellCompleteContainerNames(cmd, statusFilterFn) + return completion.ShellCompleteContainerNames(cmd, statusFilterFn) } diff --git a/cmd/nerdctl/history.go b/cmd/nerdctl/image/history.go similarity index 92% rename from cmd/nerdctl/history.go rename to cmd/nerdctl/image/history.go index 27f1ff58b22..b36b91a0f1b 100644 --- a/cmd/nerdctl/history.go +++ b/cmd/nerdctl/image/history.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package image import ( "bytes" @@ -29,6 +29,10 @@ import ( "github.com/containerd/containerd" "github.com/containerd/containerd/pkg/progress" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/cmd/nerdctl/utils" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/fmtutil" "github.com/containerd/nerdctl/pkg/formatter" "github.com/containerd/nerdctl/pkg/idutil/imagewalker" "github.com/containerd/nerdctl/pkg/imgutil" @@ -37,11 +41,11 @@ import ( "github.com/spf13/cobra" ) -func newHistoryCommand() *cobra.Command { +func NewHistoryCommand() *cobra.Command { var historyCommand = &cobra.Command{ Use: "history [flags] IMAGE", Short: "Show the history of an image", - Args: IsExactArgs(1), + Args: utils.IsExactArgs(1), RunE: historyAction, ValidArgsFunction: historyShellComplete, SilenceUsage: true, @@ -69,7 +73,7 @@ type historyPrintable struct { } func historyAction(cmd *cobra.Command, args []string) error { - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } @@ -195,7 +199,7 @@ func printHistory(cmd *cobra.Command, historys []historyPrintable) error { return errors.New("format and quiet must not be specified together") } var err error - tmpl, err = parseTemplate(format) + tmpl, err = fmtutil.ParseTemplate(format) if err != nil { return err } @@ -214,7 +218,7 @@ func printHistory(cmd *cobra.Command, historys []historyPrintable) error { } } - if f, ok := w.(Flusher); ok { + if f, ok := w.(fmtutil.Flusher); ok { return f.Flush() } return nil @@ -254,5 +258,5 @@ func (x *historyPrinter) printHistory(p historyPrintable) error { func historyShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // show image names - return shellCompleteImageNames(cmd) + return completion.ShellCompleteImageNames(cmd) } diff --git a/cmd/nerdctl/image.go b/cmd/nerdctl/image/image.go similarity index 55% rename from cmd/nerdctl/image.go rename to cmd/nerdctl/image/image.go index afae1936335..2d076a9ca43 100644 --- a/cmd/nerdctl/image.go +++ b/cmd/nerdctl/image/image.go @@ -14,50 +14,53 @@ limitations under the License. */ -package main +package image import ( + "github.com/containerd/nerdctl/cmd/nerdctl/builder" + "github.com/containerd/nerdctl/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/common" "github.com/spf13/cobra" ) -func newImageCommand() *cobra.Command { +func NewImageCommand() *cobra.Command { cmd := &cobra.Command{ - Annotations: map[string]string{Category: Management}, + Annotations: map[string]string{common.Category: common.Management}, Use: "image", Short: "Manage images", - RunE: unknownSubcommandAction, + RunE: completion.UnknownSubcommandAction, SilenceUsage: true, SilenceErrors: true, } cmd.AddCommand( - newBuildCommand(), + builder.NewBuildCommand(), // commitCommand is in "container", not in "image" - imageLsCommand(), - newHistoryCommand(), - newPullCommand(), - newPushCommand(), - newLoadCommand(), - newSaveCommand(), - newTagCommand(), - imageRmCommand(), - newImageConvertCommand(), - newImageInspectCommand(), - newImageEncryptCommand(), - newImageDecryptCommand(), - newImagePruneCommand(), + NewLsCommand(), + NewHistoryCommand(), + NewPullCommand(), + NewPushCommand(), + NewLoadCommand(), + NewSaveCommand(), + NewTagCommand(), + NewRmCommand(), + NewConvertCommand(), + NewInspectCommand(), + NewEncryptCommand(), + NewDecryptCommand(), + NewPruneCommand(), ) return cmd } -func imageLsCommand() *cobra.Command { - x := newImagesCommand() +func NewLsCommand() *cobra.Command { + x := NewImagesCommandForMain() x.Use = "ls" x.Aliases = []string{"list"} return x } -func imageRmCommand() *cobra.Command { - x := newRmiCommand() +func NewRmCommand() *cobra.Command { + x := NewRmiCommandForMain() x.Use = "rm" x.Aliases = []string{"remove"} return x diff --git a/cmd/nerdctl/image_convert.go b/cmd/nerdctl/image/image_convert.go similarity index 97% rename from cmd/nerdctl/image_convert.go rename to cmd/nerdctl/image/image_convert.go index 33a37bcb2fe..21e3080ce45 100644 --- a/cmd/nerdctl/image_convert.go +++ b/cmd/nerdctl/image/image_convert.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package image import ( "compress/gzip" @@ -30,6 +30,8 @@ import ( "github.com/containerd/containerd/images" "github.com/containerd/containerd/images/converter" "github.com/containerd/containerd/images/converter/uncompress" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/completion" converterutil "github.com/containerd/nerdctl/pkg/imgutil/converter" "github.com/containerd/nerdctl/pkg/platformutil" "github.com/containerd/nerdctl/pkg/referenceutil" @@ -57,7 +59,7 @@ For encryption and decryption, use 'nerdctl image (encrypt|decrypt)' command. ` // imageConvertCommand is from https://github.com/containerd/stargz-snapshotter/blob/d58f43a8235e46da73fb94a1a35280cb4d607b2c/cmd/ctr-remote/commands/convert.go -func newImageConvertCommand() *cobra.Command { +func NewConvertCommand() *cobra.Command { imageConvertCommand := &cobra.Command{ Use: "convert [flags] ...", Short: "convert an image", @@ -110,7 +112,7 @@ func newImageConvertCommand() *cobra.Command { // #region platform flags // platform is defined as StringSlice, not StringArray, to allow specifying "--platform=amd64,arm64" imageConvertCommand.Flags().StringSlice("platform", []string{}, "Convert content for a specific platform") - imageConvertCommand.RegisterFlagCompletionFunc("platform", shellCompletePlatforms) + imageConvertCommand.RegisterFlagCompletionFunc("platform", completion.ShellCompletePlatforms) imageConvertCommand.Flags().Bool("all-platforms", false, "Convert content for all platforms") // #endregion @@ -178,7 +180,7 @@ func imageConvertAction(cmd *cobra.Command, args []string) error { return err } - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } @@ -458,7 +460,7 @@ func getNydusConvertOpts(cmd *cobra.Command) (*nydusconvert.PackOption, error) { return nil, err } if workDir == "" { - workDir, err = getDataStore(cmd) + workDir, err = ncclient.DataStore(cmd) if err != nil { return nil, err } @@ -524,7 +526,7 @@ func readPathsFromRecordFile(filename string) ([]string, error) { func imageConvertShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // show image names - return shellCompleteImageNames(cmd) + return completion.ShellCompleteImageNames(cmd) } func printConvertedImage(cmd *cobra.Command, img converterutil.ConvertedImageInfo) error { diff --git a/cmd/nerdctl/image_cryptutil.go b/cmd/nerdctl/image/image_cryptutil.go similarity index 95% rename from cmd/nerdctl/image_cryptutil.go rename to cmd/nerdctl/image/image_cryptutil.go index 18d232ebb82..024d9c7aae1 100644 --- a/cmd/nerdctl/image_cryptutil.go +++ b/cmd/nerdctl/image/image_cryptutil.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package image import ( "context" @@ -25,6 +25,8 @@ import ( "github.com/containerd/containerd/images/converter" "github.com/containerd/imgcrypt/images/encryption" "github.com/containerd/imgcrypt/images/encryption/parsehelpers" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/completion" "github.com/containerd/nerdctl/pkg/platformutil" "github.com/containerd/nerdctl/pkg/referenceutil" ocispec "github.com/opencontainers/image-spec/specs-go/v1" @@ -43,7 +45,7 @@ func registerImgcryptFlags(cmd *cobra.Command, encrypt bool) { // #region platform flags // platform is defined as StringSlice, not StringArray, to allow specifying "--platform=amd64,arm64" flags.StringSlice("platform", []string{}, "Convert content for a specific platform") - cmd.RegisterFlagCompletionFunc("platform", shellCompletePlatforms) + cmd.RegisterFlagCompletionFunc("platform", completion.ShellCompletePlatforms) flags.Bool("all-platforms", false, "Convert content for all platforms") // #endregion @@ -136,7 +138,7 @@ func getImgcryptAction(encrypt bool) func(cmd *cobra.Command, args []string) err return err } - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } @@ -196,5 +198,5 @@ func composeConvertFunc(a, b converter.ConvertFunc) converter.ConvertFunc { func imgcryptShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // show image names - return shellCompleteImageNames(cmd) + return completion.ShellCompleteImageNames(cmd) } diff --git a/cmd/nerdctl/image_decrypt.go b/cmd/nerdctl/image/image_decrypt.go similarity index 97% rename from cmd/nerdctl/image_decrypt.go rename to cmd/nerdctl/image/image_decrypt.go index fa6faf46629..beeeed6645f 100644 --- a/cmd/nerdctl/image_decrypt.go +++ b/cmd/nerdctl/image/image_decrypt.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package image import ( "github.com/spf13/cobra" @@ -45,7 +45,7 @@ Example (decrypt): nerdctl image decrypt --key=mykey.pem example.com/foo:encrypted foo:decrypted ` -func newImageDecryptCommand() *cobra.Command { +func NewDecryptCommand() *cobra.Command { cmd := &cobra.Command{ Use: "decrypt [flags] ...", Short: "decrypt an image", diff --git a/cmd/nerdctl/image_encrypt.go b/cmd/nerdctl/image/image_encrypt.go similarity index 97% rename from cmd/nerdctl/image_encrypt.go rename to cmd/nerdctl/image/image_encrypt.go index 39ee30659f3..9760c46edb2 100644 --- a/cmd/nerdctl/image_encrypt.go +++ b/cmd/nerdctl/image/image_encrypt.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package image import ( "github.com/spf13/cobra" @@ -45,7 +45,7 @@ CAUTION: This command only encrypts image layers, but does NOT encrypt container To see non-encrypted information, run 'nerdctl image inspect --mode=native --platform=PLATFORM example.com/foo:encrypted' . ` -func newImageEncryptCommand() *cobra.Command { +func NewEncryptCommand() *cobra.Command { cmd := &cobra.Command{ Use: "encrypt [flags] ...", Short: "encrypt image layers", diff --git a/cmd/nerdctl/image_inspect.go b/cmd/nerdctl/image/image_inspect.go similarity index 86% rename from cmd/nerdctl/image_inspect.go rename to cmd/nerdctl/image/image_inspect.go index 15e5bf912fa..64858da324b 100644 --- a/cmd/nerdctl/image_inspect.go +++ b/cmd/nerdctl/image/image_inspect.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package image import ( "context" @@ -23,6 +23,9 @@ import ( "github.com/containerd/containerd" "github.com/containerd/containerd/platforms" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/fmtutil" "github.com/containerd/nerdctl/pkg/idutil/imagewalker" "github.com/containerd/nerdctl/pkg/imageinspector" "github.com/containerd/nerdctl/pkg/inspecttypes/dockercompat" @@ -30,7 +33,7 @@ import ( "github.com/spf13/cobra" ) -func newImageInspectCommand() *cobra.Command { +func NewInspectCommand() *cobra.Command { var imageInspectCommand = &cobra.Command{ Use: "inspect [flags] IMAGE [IMAGE...]", Args: cobra.MinimumNArgs(1), @@ -52,7 +55,7 @@ func newImageInspectCommand() *cobra.Command { // #region platform flags imageInspectCommand.Flags().String("platform", "", "Inspect a specific platform") // not a slice, and there is no --all-platforms - imageInspectCommand.RegisterFlagCompletionFunc("platform", shellCompletePlatforms) + imageInspectCommand.RegisterFlagCompletionFunc("platform", completion.ShellCompletePlatforms) // #endregion return imageInspectCommand @@ -63,10 +66,10 @@ func imageInspectAction(cmd *cobra.Command, args []string) error { if err != nil { return err } - return imageInspectActionWithPlatform(cmd, args, platform) + return InspectActionWithPlatform(cmd, args, platform) } -func imageInspectActionWithPlatform(cmd *cobra.Command, args []string, platform string) error { +func InspectActionWithPlatform(cmd *cobra.Command, args []string, platform string) error { var clientOpts []containerd.ClientOpt if platform != "" { platformParsed, err := platforms.Parse(platform) @@ -76,7 +79,7 @@ func imageInspectActionWithPlatform(cmd *cobra.Command, args []string, platform platformM := platforms.Only(platformParsed) clientOpts = append(clientOpts, containerd.WithDefaultPlatform(platformM)) } - client, ctx, cancel, err := newClient(cmd, clientOpts...) + client, ctx, cancel, err := ncclient.New(cmd, clientOpts...) if err != nil { return err } @@ -129,7 +132,7 @@ func imageInspectActionWithPlatform(cmd *cobra.Command, args []string, platform return fmt.Errorf("%d errors: %v", len(errs), errs) } - return formatSlice(cmd, f.entries) + return fmtutil.FormatSlice(cmd, f.entries) } type imageInspector struct { @@ -139,5 +142,5 @@ type imageInspector struct { func imageInspectShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // show image names - return shellCompleteImageNames(cmd) + return completion.ShellCompleteImageNames(cmd) } diff --git a/cmd/nerdctl/image_prune.go b/cmd/nerdctl/image/image_prune.go similarity index 92% rename from cmd/nerdctl/image_prune.go rename to cmd/nerdctl/image/image_prune.go index ee869a183e0..6ca1734eab6 100644 --- a/cmd/nerdctl/image_prune.go +++ b/cmd/nerdctl/image/image_prune.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package image import ( "context" @@ -24,12 +24,13 @@ import ( "github.com/containerd/containerd" "github.com/containerd/containerd/images" "github.com/containerd/containerd/platforms" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" "github.com/opencontainers/go-digest" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) -func newImagePruneCommand() *cobra.Command { +func NewPruneCommand() *cobra.Command { imagePruneCommand := &cobra.Command{ Use: "prune [flags]", Short: "Remove unused images", @@ -74,16 +75,16 @@ func imagePruneAction(cmd *cobra.Command, _ []string) error { } } - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } defer cancel() - return imagePrune(ctx, cmd, client) + return Prune(ctx, cmd, client) } -func imagePrune(ctx context.Context, cmd *cobra.Command, client *containerd.Client) error { +func Prune(ctx context.Context, cmd *cobra.Command, client *containerd.Client) error { var ( imageStore = client.ImageService() contentStore = client.ContentStore() diff --git a/cmd/nerdctl/images.go b/cmd/nerdctl/image/images.go similarity index 88% rename from cmd/nerdctl/images.go rename to cmd/nerdctl/image/images.go index 60ffd9165e6..eeb299df8d9 100644 --- a/cmd/nerdctl/images.go +++ b/cmd/nerdctl/image/images.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package image import ( "bytes" @@ -31,22 +31,25 @@ import ( "github.com/containerd/containerd" "github.com/containerd/containerd/content" - "github.com/containerd/containerd/errdefs" "github.com/containerd/containerd/images" "github.com/containerd/containerd/pkg/progress" "github.com/containerd/containerd/platforms" dockerreference "github.com/containerd/containerd/reference/docker" "github.com/containerd/containerd/snapshots" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/cmd/nerdctl/utils" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/fmtutil" "github.com/containerd/nerdctl/pkg/formatter" "github.com/containerd/nerdctl/pkg/imgutil" "github.com/containerd/nerdctl/pkg/referenceutil" - "github.com/opencontainers/image-spec/identity" v1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) -func newImagesCommand() *cobra.Command { +// NewImagesCommandForMain is a top-level subcommand. +func NewImagesCommandForMain() *cobra.Command { shortHelp := "List images" longHelp := shortHelp + ` @@ -98,7 +101,7 @@ func imagesAction(cmd *cobra.Command, args []string) error { filters = append(filters, fmt.Sprintf("name==%s", canonicalRef.String())) filters = append(filters, fmt.Sprintf("name==%s", args[0])) } - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } @@ -267,7 +270,7 @@ func printImages(ctx context.Context, cmd *cobra.Command, client *containerd.Cli return errors.New("format and quiet must not be specified together") } var err error - tmpl, err = parseTemplate(format) + tmpl, err = fmtutil.ParseTemplate(format) if err != nil { return err } @@ -295,7 +298,7 @@ func printImages(ctx context.Context, cmd *cobra.Command, client *containerd.Cli logrus.Warn(err) } } - if f, ok := w.(Flusher); ok { + if f, ok := w.(fmtutil.Flusher); ok { return f.Flush() } return nil @@ -350,7 +353,7 @@ func (x *imagePrinter) printImageSinglePlatform(ctx context.Context, img images. logrus.WithError(err).Warnf("failed to get blob size of image %q for platform %q", img.Name, platforms.Format(ociPlatform)) } - size, err := unpackedImageSize(ctx, x.snapshotter, image) + size, err := utils.UnpackedImageSize(ctx, x.snapshotter, image) if err != nil { logrus.WithError(err).Warnf("failed to get unpacked size of image %q for platform %q", img.Name, platforms.Format(ociPlatform)) } @@ -416,60 +419,7 @@ func (x *imagePrinter) printImageSinglePlatform(ctx context.Context, img images. func imagesShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(args) == 0 { // show image names - return shellCompleteImageNames(cmd) + return completion.ShellCompleteImageNames(cmd) } return nil, cobra.ShellCompDirectiveNoFileComp } - -type snapshotKey string - -// recursive function to calculate total usage of key's parent -func (key snapshotKey) add(ctx context.Context, s snapshots.Snapshotter, usage *snapshots.Usage) error { - if key == "" { - return nil - } - u, err := s.Usage(ctx, string(key)) - if err != nil { - return err - } - - usage.Add(u) - - info, err := s.Stat(ctx, string(key)) - if err != nil { - return err - } - - key = snapshotKey(info.Parent) - return key.add(ctx, s, usage) -} - -// unpackedImageSize is the size of the unpacked snapshots. -// Does not contain the size of the blobs in the content store. (Corresponds to Docker). -func unpackedImageSize(ctx context.Context, s snapshots.Snapshotter, img containerd.Image) (int64, error) { - diffIDs, err := img.RootFS(ctx) - if err != nil { - return 0, err - } - - chainID := identity.ChainID(diffIDs).String() - usage, err := s.Usage(ctx, chainID) - if err != nil { - if errdefs.IsNotFound(err) { - logrus.WithError(err).Debugf("image %q seems not unpacked", img.Name()) - return 0, nil - } - return 0, err - } - - info, err := s.Stat(ctx, chainID) - if err != nil { - return 0, err - } - - //add ChainID's parent usage to the total usage - if err := snapshotKey(info.Parent).add(ctx, s, &usage); err != nil { - return 0, err - } - return usage.Size, nil -} diff --git a/cmd/nerdctl/load.go b/cmd/nerdctl/image/load.go similarity index 54% rename from cmd/nerdctl/load.go rename to cmd/nerdctl/image/load.go index 02e43d559c3..482025ecaed 100644 --- a/cmd/nerdctl/load.go +++ b/cmd/nerdctl/image/load.go @@ -14,24 +14,20 @@ limitations under the License. */ -package main +package image import ( "errors" - "fmt" - "io" "os" - "github.com/containerd/containerd" "github.com/containerd/containerd/archive/compression" - "github.com/containerd/containerd/images" - "github.com/containerd/containerd/images/archive" - "github.com/containerd/containerd/platforms" + "github.com/containerd/nerdctl/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/cmd/nerdctl/utils" "github.com/containerd/nerdctl/pkg/platformutil" "github.com/spf13/cobra" ) -func newLoadCommand() *cobra.Command { +func NewLoadCommand() *cobra.Command { var loadCommand = &cobra.Command{ Use: "load", Args: cobra.NoArgs, @@ -47,7 +43,7 @@ func newLoadCommand() *cobra.Command { // #region platform flags // platform is defined as StringSlice, not StringArray, to allow specifying "--platform=amd64,arm64" loadCommand.Flags().StringSlice("platform", []string{}, "Import content for a specific platform") - loadCommand.RegisterFlagCompletionFunc("platform", shellCompletePlatforms) + loadCommand.RegisterFlagCompletionFunc("platform", completion.ShellCompletePlatforms) loadCommand.Flags().Bool("all-platforms", false, "Import content for all platforms") // #endregion @@ -95,65 +91,5 @@ func loadAction(cmd *cobra.Command, _ []string) error { return err } - return loadImage(decompressor, cmd, platMC, false) -} - -func loadImage(in io.Reader, cmd *cobra.Command, platMC platforms.MatchComparer, quiet bool) error { - // In addition to passing WithImagePlatform() to client.Import(), we also need to pass WithDefaultPlatform() to newClient(). - // Otherwise unpacking may fail. - client, ctx, cancel, err := newClient(cmd, containerd.WithDefaultPlatform(platMC)) - if err != nil { - return err - } - defer cancel() - - sn, err := cmd.Flags().GetString("snapshotter") - if err != nil { - return err - } - - r := &readCounter{Reader: in} - imgs, err := client.Import(ctx, r, containerd.WithDigestRef(archive.DigestTranslator(sn)), containerd.WithSkipDigestRef(func(name string) bool { return name != "" }), containerd.WithImportPlatform(platMC)) - if err != nil { - if r.N == 0 { - // Avoid confusing "unrecognized image format" - return errors.New("no image was built") - } - if errors.Is(err, images.ErrEmptyWalk) { - err = fmt.Errorf("%w (Hint: set `--platform=PLATFORM` or `--all-platforms`)", err) - } - return err - } - for _, img := range imgs { - image := containerd.NewImageWithPlatform(client, img, platMC) - - // TODO: Show unpack status - if !quiet { - fmt.Fprintf(cmd.OutOrStdout(), "unpacking %s (%s)...\n", img.Name, img.Target.Digest) - } - err = image.Unpack(ctx, sn) - if err != nil { - return err - } - if quiet { - fmt.Fprintln(cmd.OutOrStdout(), img.Target.Digest) - } else { - fmt.Fprintf(cmd.OutOrStdout(), "Loaded image: %s\n", img.Name) - } - } - - return nil -} - -type readCounter struct { - io.Reader - N int -} - -func (r *readCounter) Read(p []byte) (int, error) { - n, err := r.Reader.Read(p) - if n > 0 { - r.N += n - } - return n, err + return utils.LoadImage(decompressor, cmd, platMC, false) } diff --git a/cmd/nerdctl/pull.go b/cmd/nerdctl/image/pull.go similarity index 52% rename from cmd/nerdctl/pull.go rename to cmd/nerdctl/image/pull.go index c3ea62b9956..f8bba256b29 100644 --- a/cmd/nerdctl/pull.go +++ b/cmd/nerdctl/image/pull.go @@ -14,32 +14,22 @@ limitations under the License. */ -package main +package image import ( - "context" - "errors" - "fmt" - - "github.com/containerd/containerd" - "github.com/containerd/nerdctl/pkg/cosignutil" - "github.com/containerd/nerdctl/pkg/imgutil" - "github.com/containerd/nerdctl/pkg/ipfs" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/cmd/nerdctl/utils" "github.com/containerd/nerdctl/pkg/platformutil" - "github.com/containerd/nerdctl/pkg/referenceutil" "github.com/containerd/nerdctl/pkg/strutil" - httpapi "github.com/ipfs/go-ipfs-http-client" - v1 "github.com/opencontainers/image-spec/specs-go/v1" - "github.com/sirupsen/logrus" - "github.com/spf13/cobra" ) -func newPullCommand() *cobra.Command { +func NewPullCommand() *cobra.Command { var pullCommand = &cobra.Command{ Use: "pull [flags] NAME[:TAG]", Short: "Pull an image from a registry. Optionally specify \"ipfs://\" or \"ipns://\" scheme to pull image from IPFS.", - Args: IsExactArgs(1), + Args: utils.IsExactArgs(1), RunE: pullAction, SilenceUsage: true, SilenceErrors: true, @@ -52,7 +42,7 @@ func newPullCommand() *cobra.Command { // #region platform flags // platform is defined as StringSlice, not StringArray, to allow specifying "--platform=amd64,arm64" pullCommand.Flags().StringSlice("platform", nil, "Pull content for a specific platform") - pullCommand.RegisterFlagCompletionFunc("platform", shellCompletePlatforms) + pullCommand.RegisterFlagCompletionFunc("platform", completion.ShellCompletePlatforms) pullCommand.Flags().Bool("all-platforms", false, "Pull content for all platforms") // #endregion @@ -71,7 +61,7 @@ func newPullCommand() *cobra.Command { func pullAction(cmd *cobra.Command, args []string) error { rawRef := args[0] - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } @@ -102,83 +92,10 @@ func pullAction(cmd *cobra.Command, args []string) error { return err } - _, err = ensureImage(ctx, cmd, client, rawRef, ocispecPlatforms, "always", unpack, quiet) + _, err = utils.EnsureImage(ctx, cmd, client, rawRef, ocispecPlatforms, "always", unpack, quiet) if err != nil { return err } return nil } - -func ensureImage(ctx context.Context, cmd *cobra.Command, client *containerd.Client, rawRef string, ocispecPlatforms []v1.Platform, - pull string, unpack *bool, quiet bool) (*imgutil.EnsuredImage, error) { - - var ensured *imgutil.EnsuredImage - snapshotter, err := cmd.Flags().GetString("snapshotter") - if err != nil { - return nil, err - } - insecureRegistry, err := cmd.Flags().GetBool("insecure-registry") - if err != nil { - return nil, err - } - hostsDirs, err := cmd.Flags().GetStringSlice("hosts-dir") - if err != nil { - return nil, err - } - verifier, err := cmd.Flags().GetString("verify") - if err != nil { - return nil, err - } - - if scheme, ref, err := referenceutil.ParseIPFSRefWithScheme(rawRef); err == nil { - if verifier != "none" { - return nil, errors.New("--verify flag is not supported on IPFS as of now") - } - - ipfsClient, err := httpapi.NewLocalApi() - if err != nil { - return nil, err - } - ensured, err = ipfs.EnsureImage(ctx, client, ipfsClient, cmd.OutOrStdout(), cmd.ErrOrStderr(), snapshotter, scheme, ref, - pull, ocispecPlatforms, unpack, quiet) - if err != nil { - return nil, err - } - return ensured, nil - } - - ref := rawRef - switch verifier { - case "cosign": - experimental, err := cmd.Flags().GetBool("experimental") - if err != nil { - return nil, err - } - - if !experimental { - return nil, fmt.Errorf("cosign only work with enable experimental feature") - } - - keyRef, err := cmd.Flags().GetString("cosign-key") - if err != nil { - return nil, err - } - - ref, err = cosignutil.VerifyCosign(ctx, rawRef, keyRef, hostsDirs) - if err != nil { - return nil, err - } - case "none": - logrus.Debugf("verification process skipped") - default: - return nil, fmt.Errorf("no verifier found: %s", verifier) - } - - ensured, err = imgutil.EnsureImage(ctx, client, cmd.OutOrStdout(), cmd.ErrOrStderr(), snapshotter, ref, - pull, insecureRegistry, hostsDirs, ocispecPlatforms, unpack, quiet) - if err != nil { - return nil, err - } - return ensured, err -} diff --git a/cmd/nerdctl/push.go b/cmd/nerdctl/image/push.go similarity index 95% rename from cmd/nerdctl/push.go rename to cmd/nerdctl/image/push.go index c467014a243..5376668e457 100644 --- a/cmd/nerdctl/push.go +++ b/cmd/nerdctl/image/push.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package image import ( "context" @@ -26,6 +26,9 @@ import ( "github.com/containerd/containerd/images/converter" refdocker "github.com/containerd/containerd/reference/docker" "github.com/containerd/containerd/remotes" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/cmd/nerdctl/utils" "github.com/containerd/nerdctl/pkg/cosignutil" "github.com/containerd/nerdctl/pkg/errutil" "github.com/containerd/nerdctl/pkg/imgutil/dockerconfigresolver" @@ -38,7 +41,7 @@ import ( estargzconvert "github.com/containerd/stargz-snapshotter/nativeconverter/estargz" httpapi "github.com/ipfs/go-ipfs-http-client" "github.com/multiformats/go-multiaddr" - digest "github.com/opencontainers/go-digest" + "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -48,11 +51,11 @@ const ( allowNonDistFlag = "allow-nondistributable-artifacts" ) -func newPushCommand() *cobra.Command { +func NewPushCommand() *cobra.Command { var pushCommand = &cobra.Command{ Use: "push [flags] NAME[:TAG]", Short: "Push an image or a repository to a registry. Optionally specify \"ipfs://\" or \"ipns://\" scheme to push image to IPFS.", - Args: IsExactArgs(1), + Args: utils.IsExactArgs(1), RunE: pushAction, ValidArgsFunction: pushShellComplete, SilenceUsage: true, @@ -61,7 +64,7 @@ func newPushCommand() *cobra.Command { // #region platform flags // platform is defined as StringSlice, not StringArray, to allow specifying "--platform=amd64,arm64" pushCommand.Flags().StringSlice("platform", []string{}, "Push content for a specific platform") - pushCommand.RegisterFlagCompletionFunc("platform", shellCompletePlatforms) + pushCommand.RegisterFlagCompletionFunc("platform", completion.ShellCompletePlatforms) pushCommand.Flags().Bool("all-platforms", false, "Push content for all platforms") // #endregion @@ -85,7 +88,7 @@ func newPushCommand() *cobra.Command { func pushAction(cmd *cobra.Command, args []string) error { rawRef := args[0] - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } @@ -267,7 +270,7 @@ func pushAction(cmd *cobra.Command, args []string) error { func pushShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // show image names - return shellCompleteImageNames(cmd) + return completion.ShellCompleteImageNames(cmd) } func eStargzConvertFunc() converter.ConvertFunc { diff --git a/cmd/nerdctl/rmi.go b/cmd/nerdctl/image/rmi.go similarity index 92% rename from cmd/nerdctl/rmi.go rename to cmd/nerdctl/image/rmi.go index 4bbf1e26499..1ef24071128 100644 --- a/cmd/nerdctl/rmi.go +++ b/cmd/nerdctl/image/rmi.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package image import ( "context" @@ -23,13 +23,16 @@ import ( "github.com/containerd/containerd/images" "github.com/containerd/containerd/platforms" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/completion" "github.com/containerd/nerdctl/pkg/formatter" "github.com/containerd/nerdctl/pkg/idutil/imagewalker" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) -func newRmiCommand() *cobra.Command { +// NewRmiCommandForMain is a top-level subcommand. +func NewRmiCommandForMain() *cobra.Command { var rmiCommand = &cobra.Command{ Use: "rmi [flags] IMAGE [IMAGE, ...]", Short: "Remove one or more images", @@ -58,7 +61,7 @@ func rmiAction(cmd *cobra.Command, args []string) error { delOpts = append(delOpts, images.SynchronousDelete()) } - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } @@ -133,5 +136,5 @@ func rmiAction(cmd *cobra.Command, args []string) error { func rmiShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // show image names - return shellCompleteImageNames(cmd) + return completion.ShellCompleteImageNames(cmd) } diff --git a/cmd/nerdctl/save.go b/cmd/nerdctl/image/save.go similarity index 90% rename from cmd/nerdctl/save.go rename to cmd/nerdctl/image/save.go index b7623e1e55e..06bca0a84a4 100644 --- a/cmd/nerdctl/save.go +++ b/cmd/nerdctl/image/save.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package image import ( "fmt" @@ -22,6 +22,8 @@ import ( "os" "github.com/containerd/containerd/images/archive" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/completion" "github.com/containerd/nerdctl/pkg/platformutil" "github.com/containerd/nerdctl/pkg/referenceutil" "github.com/mattn/go-isatty" @@ -29,7 +31,7 @@ import ( "github.com/spf13/cobra" ) -func newSaveCommand() *cobra.Command { +func NewSaveCommand() *cobra.Command { var saveCommand = &cobra.Command{ Use: "save", Args: cobra.MinimumNArgs(1), @@ -45,7 +47,7 @@ func newSaveCommand() *cobra.Command { // #region platform flags // platform is defined as StringSlice, not StringArray, to allow specifying "--platform=amd64,arm64" saveCommand.Flags().StringSlice("platform", []string{}, "Export content for a specific platform") - saveCommand.RegisterFlagCompletionFunc("platform", shellCompletePlatforms) + saveCommand.RegisterFlagCompletionFunc("platform", completion.ShellCompletePlatforms) saveCommand.Flags().Bool("all-platforms", false, "Export content for all platforms") // #endregion @@ -83,7 +85,7 @@ func saveAction(cmd *cobra.Command, args []string) error { } func saveImage(images []string, out io.Writer, saveOpts []archive.ExportOpt, cmd *cobra.Command) error { - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } @@ -118,5 +120,5 @@ func saveImage(images []string, out io.Writer, saveOpts []archive.ExportOpt, cmd func saveShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // show image names - return shellCompleteImageNames(cmd) + return completion.ShellCompleteImageNames(cmd) } diff --git a/cmd/nerdctl/tag.go b/cmd/nerdctl/image/tag.go similarity index 87% rename from cmd/nerdctl/tag.go rename to cmd/nerdctl/image/tag.go index d8a8bbd909b..4010e79a158 100644 --- a/cmd/nerdctl/tag.go +++ b/cmd/nerdctl/image/tag.go @@ -14,24 +14,27 @@ limitations under the License. */ -package main +package image import ( "context" "fmt" "github.com/containerd/containerd/errdefs" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/cmd/nerdctl/utils" "github.com/containerd/nerdctl/pkg/idutil/imagewalker" "github.com/containerd/nerdctl/pkg/referenceutil" "github.com/spf13/cobra" ) -func newTagCommand() *cobra.Command { +func NewTagCommand() *cobra.Command { var tagCommand = &cobra.Command{ Use: "tag [flags] SOURCE_IMAGE[:TAG] TARGET_IMAGE[:TAG]", Short: "Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE", - Args: IsExactArgs(2), + Args: utils.IsExactArgs(2), RunE: tagAction, ValidArgsFunction: tagShellComplete, SilenceUsage: true, @@ -41,7 +44,7 @@ func newTagCommand() *cobra.Command { } func tagAction(cmd *cobra.Command, args []string) error { - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } @@ -100,7 +103,7 @@ func tagAction(cmd *cobra.Command, args []string) error { func tagShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(args) < 2 { // show image names - return shellCompleteImageNames(cmd) + return completion.ShellCompleteImageNames(cmd) } return nil, cobra.ShellCompDirectiveNoFileComp } diff --git a/cmd/nerdctl/inspect.go b/cmd/nerdctl/inspect/inspect.go similarity index 86% rename from cmd/nerdctl/inspect.go rename to cmd/nerdctl/inspect/inspect.go index 0bb29b7939e..2f2f1763c06 100644 --- a/cmd/nerdctl/inspect.go +++ b/cmd/nerdctl/inspect/inspect.go @@ -14,19 +14,23 @@ limitations under the License. */ -package main +package inspect import ( "context" "fmt" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/cmd/nerdctl/container" + "github.com/containerd/nerdctl/cmd/nerdctl/image" "github.com/containerd/nerdctl/pkg/idutil/containerwalker" "github.com/containerd/nerdctl/pkg/idutil/imagewalker" "github.com/spf13/cobra" ) -func newInspectCommand() *cobra.Command { +func NewInspectCommand() *cobra.Command { var inspectCommand = &cobra.Command{ Use: "inspect", Short: "Return low-level information on objects.", @@ -72,7 +76,7 @@ func inspectAction(cmd *cobra.Command, args []string) error { return fmt.Errorf("%q is not a valid value for --type", inspectType) } - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } @@ -115,13 +119,13 @@ func inspectAction(cmd *cobra.Command, args []string) error { return fmt.Errorf("no such object %s", req) } if nc != 0 { - if err := containerInspectAction(cmd, []string{req}); err != nil { + if err := container.InspectAction(cmd, []string{req}); err != nil { errs = append(errs, err) } continue } if ni != 0 { - if err := imageInspectActionWithPlatform(cmd, []string{req}, ""); err != nil { + if err := image.InspectActionWithPlatform(cmd, []string{req}, ""); err != nil { errs = append(errs, err) } continue @@ -137,8 +141,8 @@ func inspectAction(cmd *cobra.Command, args []string) error { func inspectShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // show container names - containers, _ := shellCompleteContainerNames(cmd, nil) + containers, _ := completion.ShellCompleteContainerNames(cmd, nil) // show image names - images, _ := shellCompleteImageNames(cmd) + images, _ := completion.ShellCompleteImageNames(cmd) return append(containers, images...), cobra.ShellCompDirectiveNoFileComp } diff --git a/cmd/nerdctl/internal.go b/cmd/nerdctl/internal/internal.go similarity index 90% rename from cmd/nerdctl/internal.go rename to cmd/nerdctl/internal/internal.go index 9f62d1ea2ae..cf8f70bf9f4 100644 --- a/cmd/nerdctl/internal.go +++ b/cmd/nerdctl/internal/internal.go @@ -14,13 +14,13 @@ limitations under the License. */ -package main +package internal import ( "github.com/spf13/cobra" ) -func newInternalCommand() *cobra.Command { +func NewInternalCommand() *cobra.Command { var internalCommand = &cobra.Command{ Use: "internal", Short: "DO NOT EXECUTE MANUALLY", @@ -30,7 +30,7 @@ func newInternalCommand() *cobra.Command { } internalCommand.AddCommand( - newInternalOCIHookCommandCommand(), + newOCIHookCommand(), ) return internalCommand diff --git a/cmd/nerdctl/internal_oci_hook.go b/cmd/nerdctl/internal/internal_oci_hook.go similarity index 90% rename from cmd/nerdctl/internal_oci_hook.go rename to cmd/nerdctl/internal/internal_oci_hook.go index 486c11664c7..88c2bcc0857 100644 --- a/cmd/nerdctl/internal_oci_hook.go +++ b/cmd/nerdctl/internal/internal_oci_hook.go @@ -14,18 +14,19 @@ limitations under the License. */ -package main +package internal import ( "errors" "os" + "github.com/containerd/nerdctl/cmd/nerdctl/client" "github.com/containerd/nerdctl/pkg/ocihook" "github.com/spf13/cobra" ) -func newInternalOCIHookCommandCommand() *cobra.Command { +func newOCIHookCommand() *cobra.Command { var internalOCIHookCommand = &cobra.Command{ Use: "oci-hook", Short: "OCI hook", @@ -44,7 +45,7 @@ func internalOCIHookAction(cmd *cobra.Command, args []string) error { if event == "" { return errors.New("event type needs to be passed") } - dataStore, err := getDataStore(cmd) + dataStore, err := client.DataStore(cmd) if err != nil { return err } diff --git a/cmd/nerdctl/ipfs.go b/cmd/nerdctl/ipfs/ipfs.go similarity index 68% rename from cmd/nerdctl/ipfs.go rename to cmd/nerdctl/ipfs/ipfs.go index 871077e1e5f..2aaf5f750ff 100644 --- a/cmd/nerdctl/ipfs.go +++ b/cmd/nerdctl/ipfs/ipfs.go @@ -14,23 +14,26 @@ limitations under the License. */ -package main +package ipfs import ( + "github.com/containerd/nerdctl/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/cmd/nerdctl/ipfs/registry" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/common" "github.com/spf13/cobra" ) -func newIPFSCommand() *cobra.Command { +func NewIPFSCommand() *cobra.Command { cmd := &cobra.Command{ - Annotations: map[string]string{Category: Management}, + Annotations: map[string]string{common.Category: common.Management}, Use: "ipfs", Short: "Distributing images on IPFS", - RunE: unknownSubcommandAction, + RunE: completion.UnknownSubcommandAction, SilenceUsage: true, SilenceErrors: true, } cmd.AddCommand( - newIPFSRegistryCommand(), + registry.NewRegistryCommand(), ) return cmd } diff --git a/cmd/nerdctl/ipfs_registry.go b/cmd/nerdctl/ipfs/registry/ipfs_registry.go similarity index 64% rename from cmd/nerdctl/ipfs_registry.go rename to cmd/nerdctl/ipfs/registry/ipfs_registry.go index c5d72d08569..e83364dce2d 100644 --- a/cmd/nerdctl/ipfs_registry.go +++ b/cmd/nerdctl/ipfs/registry/ipfs_registry.go @@ -14,26 +14,29 @@ limitations under the License. */ -package main +package registry import ( + "github.com/containerd/nerdctl/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/cmd/nerdctl/utils" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/common" "github.com/spf13/cobra" ) -func newIPFSRegistryCommand() *cobra.Command { +func NewRegistryCommand() *cobra.Command { cmd := &cobra.Command{ - Annotations: map[string]string{Category: Management}, + Annotations: map[string]string{common.Category: common.Management}, Use: "registry", Short: "Manage read-only registry backed by IPFS", - PreRunE: checkExperimental("ipfs"), - RunE: unknownSubcommandAction, + PreRunE: utils.CheckExperimental("ipfs"), + RunE: completion.UnknownSubcommandAction, SilenceUsage: true, SilenceErrors: true, } cmd.AddCommand( - newIPFSRegistryServeCommand(), - newIPFSRegistryUpCommand(), - newIPFSRegistryDownCommand(), + NewServeCommand(), + NewUpCommand(), + NewDownCommand(), ) return cmd } diff --git a/cmd/nerdctl/ipfs_registry_down.go b/cmd/nerdctl/ipfs/registry/ipfs_registry_down.go similarity index 87% rename from cmd/nerdctl/ipfs_registry_down.go rename to cmd/nerdctl/ipfs/registry/ipfs_registry_down.go index 5ccecda4d05..7b95112b3fd 100644 --- a/cmd/nerdctl/ipfs_registry_down.go +++ b/cmd/nerdctl/ipfs/registry/ipfs_registry_down.go @@ -14,18 +14,20 @@ limitations under the License. */ -package main +package registry import ( "context" "fmt" "os/exec" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/utils" "github.com/containerd/nerdctl/pkg/idutil/containerwalker" "github.com/spf13/cobra" ) -func newIPFSRegistryDownCommand() *cobra.Command { +func NewDownCommand() *cobra.Command { var ipfsRegistryDownCommand = &cobra.Command{ Use: "down", Short: "stop registry as a background container \"ipfs-registry\".", @@ -38,7 +40,7 @@ func newIPFSRegistryDownCommand() *cobra.Command { } func ipfsRegistryDownAction(cmd *cobra.Command, args []string) error { - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } @@ -56,7 +58,7 @@ func ipfsRegistryDownAction(cmd *cobra.Command, args []string) error { if nc == 0 { return fmt.Errorf("ipfs registry %q doesn't exist", ipfsRegistryContainerName) } - nerdctlCmd, nerdctlArgs := globalFlags(cmd) + nerdctlCmd, nerdctlArgs := utils.GlobalFlags(cmd) if out, err := exec.Command(nerdctlCmd, append(nerdctlArgs, "stop", ipfsRegistryContainerName)...).CombinedOutput(); err != nil { return fmt.Errorf("failed to stop registry: %v: %v", string(out), err) } diff --git a/cmd/nerdctl/ipfs_registry_serve.go b/cmd/nerdctl/ipfs/registry/ipfs_registry_serve.go similarity index 97% rename from cmd/nerdctl/ipfs_registry_serve.go rename to cmd/nerdctl/ipfs/registry/ipfs_registry_serve.go index 51214d10fbf..6deaa2a263a 100644 --- a/cmd/nerdctl/ipfs_registry_serve.go +++ b/cmd/nerdctl/ipfs/registry/ipfs_registry_serve.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package registry import ( "fmt" @@ -33,7 +33,7 @@ const ( defaultIPFSReadTimeoutDuration = 0 ) -func newIPFSRegistryServeCommand() *cobra.Command { +func NewServeCommand() *cobra.Command { var ipfsRegistryServeCommand = &cobra.Command{ Use: "serve", Short: "serve read-only registry backed by IPFS on localhost. Use \"nerdctl ipfs registry up\".", diff --git a/cmd/nerdctl/ipfs_registry_up.go b/cmd/nerdctl/ipfs/registry/ipfs_registry_up.go similarity index 92% rename from cmd/nerdctl/ipfs_registry_up.go rename to cmd/nerdctl/ipfs/registry/ipfs_registry_up.go index d6085027cf6..45ea0466a5f 100644 --- a/cmd/nerdctl/ipfs_registry_up.go +++ b/cmd/nerdctl/ipfs/registry/ipfs_registry_up.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package registry import ( "context" @@ -25,6 +25,8 @@ import ( "path/filepath" "time" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/utils" "github.com/containerd/nerdctl/pkg/idutil/containerwalker" "github.com/hashicorp/go-multierror" httpapi "github.com/ipfs/go-ipfs-http-client" @@ -34,10 +36,10 @@ import ( const ipfsRegistryContainerName = "ipfs-registry" -func newIPFSRegistryUpCommand() *cobra.Command { +func NewUpCommand() *cobra.Command { var ipfsRegistryUpCommand = &cobra.Command{ Use: "up", - Short: "start registry as a background container \"ipfs-registry\", backed by the current user's IPFS API", + Short: "Start registry as a background container \"ipfs-registry\", backed by the current user's IPFS API", RunE: ipfsRegistryUpAction, SilenceUsage: true, SilenceErrors: true, @@ -51,7 +53,7 @@ func newIPFSRegistryUpCommand() *cobra.Command { } func ipfsRegistryUpAction(cmd *cobra.Command, args []string) error { - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } @@ -92,11 +94,11 @@ func runRegistryAsContainer(cmd *cobra.Command) error { if err != nil { return err } - dataStore, err := getDataStore(cmd) + dataStore, err := ncclient.DataStore(cmd) if err != nil { return err } - nerdctlCmd, nerdctlArgs := globalFlags(cmd) + nerdctlCmd, nerdctlArgs := utils.GlobalFlags(cmd) registryRoot := filepath.Join(dataStore, "ipfs-registry", "rootfs") if err := os.RemoveAll(registryRoot); err != nil { return err diff --git a/cmd/nerdctl/login.go b/cmd/nerdctl/login/login.go similarity index 96% rename from cmd/nerdctl/login.go rename to cmd/nerdctl/login/login.go index 78d954411eb..c88f16b4350 100644 --- a/cmd/nerdctl/login.go +++ b/cmd/nerdctl/login/login.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package login import ( "bufio" @@ -56,7 +56,7 @@ https://docs.docker.com/engine/reference/commandline/login/#credentials-store var options = new(loginOptions) -func newLoginCommand() *cobra.Command { +func NewLoginCommand() *cobra.Command { var loginCommand = &cobra.Command{ Use: "login [flags] [SERVER]", Args: cobra.MaximumNArgs(1), @@ -96,7 +96,7 @@ func loginAction(cmd *cobra.Command, args []string) error { ctx := cmd.Context() isDefaultRegistry := serverAddress == dockerconfigresolver.IndexServer - authConfig, err := GetDefaultAuthConfig(options.username == "" && options.password == "", serverAddress, isDefaultRegistry) + authConfig, err := DefaultAuthConfig(options.username == "" && options.password == "", serverAddress, isDefaultRegistry) if authConfig == nil { authConfig = &types.AuthConfig{ServerAddress: serverAddress} } @@ -174,10 +174,10 @@ func verifyloginOptions(cmd *cobra.Command, options *loginOptions) error { } -// Code from github.com/docker/cli/cli/command (v20.10.3) -// GetDefaultAuthConfig gets the default auth config given a serverAddress +// DefaultAuthConfig gets the default auth config given a serverAddress // If credentials for given serverAddress exists in the credential store, the configuration will be populated with values in it -func GetDefaultAuthConfig(checkCredStore bool, serverAddress string, isDefaultRegistry bool) (*types.AuthConfig, error) { +// Code from github.com/docker/cli/cli/command (v20.10.3) +func DefaultAuthConfig(checkCredStore bool, serverAddress string, isDefaultRegistry bool) (*types.AuthConfig, error) { if !isDefaultRegistry { var err error serverAddress, err = convertToHostname(serverAddress) diff --git a/cmd/nerdctl/login_unix.go b/cmd/nerdctl/login/login_unix.go similarity index 98% rename from cmd/nerdctl/login_unix.go rename to cmd/nerdctl/login/login_unix.go index c117a330961..ee536be1721 100644 --- a/cmd/nerdctl/login_unix.go +++ b/cmd/nerdctl/login/login_unix.go @@ -16,7 +16,7 @@ limitations under the License. */ -package main +package login import ( "fmt" diff --git a/cmd/nerdctl/login_windows.go b/cmd/nerdctl/login/login_windows.go similarity index 98% rename from cmd/nerdctl/login_windows.go rename to cmd/nerdctl/login/login_windows.go index 236ac17213b..89c3834fb92 100644 --- a/cmd/nerdctl/login_windows.go +++ b/cmd/nerdctl/login/login_windows.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package login import ( "fmt" diff --git a/cmd/nerdctl/logout.go b/cmd/nerdctl/logout/logout.go similarity index 98% rename from cmd/nerdctl/logout.go rename to cmd/nerdctl/logout/logout.go index 09b4ee43179..c7c03048289 100644 --- a/cmd/nerdctl/logout.go +++ b/cmd/nerdctl/logout/logout.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package logout import ( "fmt" @@ -24,7 +24,7 @@ import ( "github.com/spf13/cobra" ) -func newLogoutCommand() *cobra.Command { +func NewLogoutCommand() *cobra.Command { var logoutCommand = &cobra.Command{ Use: "logout [flags] [SERVER]", Args: cobra.MaximumNArgs(1), diff --git a/cmd/nerdctl/main.go b/cmd/nerdctl/main.go index dd192ebd958..76dcc9547db 100644 --- a/cmd/nerdctl/main.go +++ b/cmd/nerdctl/main.go @@ -21,12 +21,29 @@ import ( "fmt" "os" "runtime" - "strconv" "strings" "github.com/containerd/containerd" "github.com/containerd/containerd/defaults" "github.com/containerd/containerd/namespaces" + "github.com/containerd/nerdctl/cmd/nerdctl/apparmor" + "github.com/containerd/nerdctl/cmd/nerdctl/builder" + "github.com/containerd/nerdctl/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/cmd/nerdctl/compose" + "github.com/containerd/nerdctl/cmd/nerdctl/container" + "github.com/containerd/nerdctl/cmd/nerdctl/image" + "github.com/containerd/nerdctl/cmd/nerdctl/inspect" + "github.com/containerd/nerdctl/cmd/nerdctl/internal" + "github.com/containerd/nerdctl/cmd/nerdctl/ipfs" + "github.com/containerd/nerdctl/cmd/nerdctl/login" + "github.com/containerd/nerdctl/cmd/nerdctl/logout" + "github.com/containerd/nerdctl/cmd/nerdctl/namespace" + "github.com/containerd/nerdctl/cmd/nerdctl/network" + "github.com/containerd/nerdctl/cmd/nerdctl/system" + "github.com/containerd/nerdctl/cmd/nerdctl/utils" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/common" + version2 "github.com/containerd/nerdctl/cmd/nerdctl/version" + "github.com/containerd/nerdctl/cmd/nerdctl/volume" ncdefaults "github.com/containerd/nerdctl/pkg/defaults" "github.com/containerd/nerdctl/pkg/logging" "github.com/containerd/nerdctl/pkg/rootlessutil" @@ -38,11 +55,6 @@ import ( "github.com/spf13/pflag" ) -const ( - Category = "category" - Management = "management" -) - // usage was derived from https://github.com/spf13/cobra/blob/v1.2.1/command.go#L491-L514 func usage(c *cobra.Command) error { s := "Usage: " @@ -63,7 +75,7 @@ func usage(c *cobra.Command) error { var managementCommands, nonManagementCommands []*cobra.Command for _, f := range c.Commands() { f := f - if f.Annotations[Category] == Management { + if f.Annotations[common.Category] == common.Management { managementCommands = append(managementCommands, f) } else { nonManagementCommands = append(nonManagementCommands, f) @@ -106,7 +118,7 @@ func usage(c *cobra.Command) error { func main() { if err := xmain(); err != nil { - HandleExitCoder(err) + common.HandleExitCoder(err) logrus.Fatal(err) } } @@ -182,23 +194,23 @@ func initRootCmdFlags(rootCmd *cobra.Command, tomlPath string) (*pflag.FlagSet, rootCmd.PersistentFlags().Bool("debug", cfg.Debug, "debug mode") rootCmd.PersistentFlags().Bool("debug-full", cfg.DebugFull, "debug mode (with full output)") // -a is aliases (conflicts with nerdctl images -a) - AddPersistentStringFlag(rootCmd, "address", []string{"a", "H"}, nil, []string{"host"}, aliasToBeInherited, cfg.Address, "CONTAINERD_ADDRESS", `containerd address, optionally with "unix://" prefix`) + utils.AddPersistentStringFlag(rootCmd, "address", []string{"a", "H"}, nil, []string{"host"}, aliasToBeInherited, cfg.Address, "CONTAINERD_ADDRESS", `containerd address, optionally with "unix://" prefix`) // -n is aliases (conflicts with nerdctl logs -n) - AddPersistentStringFlag(rootCmd, "namespace", []string{"n"}, nil, nil, aliasToBeInherited, cfg.Namespace, "CONTAINERD_NAMESPACE", `containerd namespace, such as "moby" for Docker, "k8s.io" for Kubernetes`) - rootCmd.RegisterFlagCompletionFunc("namespace", shellCompleteNamespaceNames) - AddPersistentStringFlag(rootCmd, "snapshotter", nil, nil, []string{"storage-driver"}, aliasToBeInherited, cfg.Snapshotter, "CONTAINERD_SNAPSHOTTER", "containerd snapshotter") - rootCmd.RegisterFlagCompletionFunc("snapshotter", shellCompleteSnapshotterNames) - rootCmd.RegisterFlagCompletionFunc("storage-driver", shellCompleteSnapshotterNames) - AddPersistentStringFlag(rootCmd, "cni-path", nil, nil, nil, aliasToBeInherited, cfg.CNIPath, "CNI_PATH", "cni plugins binary directory") - AddPersistentStringFlag(rootCmd, "cni-netconfpath", nil, nil, nil, aliasToBeInherited, cfg.CNINetConfPath, "NETCONFPATH", "cni config directory") + utils.AddPersistentStringFlag(rootCmd, "namespace", []string{"n"}, nil, nil, aliasToBeInherited, cfg.Namespace, "CONTAINERD_NAMESPACE", `containerd namespace, such as "moby" for Docker, "k8s.io" for Kubernetes`) + rootCmd.RegisterFlagCompletionFunc("namespace", completion.ShellCompleteNamespaceNames) + utils.AddPersistentStringFlag(rootCmd, "snapshotter", nil, nil, []string{"storage-driver"}, aliasToBeInherited, cfg.Snapshotter, "CONTAINERD_SNAPSHOTTER", "containerd snapshotter") + rootCmd.RegisterFlagCompletionFunc("snapshotter", completion.ShellCompleteSnapshotterNames) + rootCmd.RegisterFlagCompletionFunc("storage-driver", completion.ShellCompleteSnapshotterNames) + utils.AddPersistentStringFlag(rootCmd, "cni-path", nil, nil, nil, aliasToBeInherited, cfg.CNIPath, "CNI_PATH", "cni plugins binary directory") + utils.AddPersistentStringFlag(rootCmd, "cni-netconfpath", nil, nil, nil, aliasToBeInherited, cfg.CNINetConfPath, "NETCONFPATH", "cni config directory") rootCmd.PersistentFlags().String("data-root", cfg.DataRoot, "Root directory of persistent nerdctl state (managed by nerdctl, not by containerd)") rootCmd.PersistentFlags().String("cgroup-manager", cfg.CgroupManager, `Cgroup manager to use ("cgroupfs"|"systemd")`) - rootCmd.RegisterFlagCompletionFunc("cgroup-manager", shellCompleteCgroupManagerNames) + rootCmd.RegisterFlagCompletionFunc("cgroup-manager", completion.ShellCompleteCgroupManagerNames) rootCmd.PersistentFlags().Bool("insecure-registry", cfg.InsecureRegistry, "skips verifying HTTPS certs, and allows falling back to plain HTTP") // hosts-dir is defined as StringSlice, not StringArray, to allow specifying "--hosts-dir=/etc/containerd/certs.d,/etc/docker/certs.d" rootCmd.PersistentFlags().StringSlice("hosts-dir", cfg.HostsDir, "A directory that contains /hosts.toml (containerd style) or /{ca.cert, cert.pem, key.pem} (docker style)") // Experimental enable experimental feature, see in https://github.com/containerd/nerdctl/blob/main/docs/experimental.md - AddPersistentBoolFlag(rootCmd, "experimental", nil, nil, cfg.Experimental, "NERDCTL_EXPERIMENTAL", "Control experimental: https://github.com/containerd/nerdctl/blob/main/docs/experimental.md") + utils.AddPersistentBoolFlag(rootCmd, "experimental", nil, nil, cfg.Experimental, "NERDCTL_EXPERIMENTAL", "Control experimental: https://github.com/containerd/nerdctl/blob/main/docs/experimental.md") return aliasToBeInherited, nil } @@ -264,85 +276,85 @@ Config file ($NERDCTL_TOML): %s } return nil } - rootCmd.RunE = unknownSubcommandAction + rootCmd.RunE = completion.UnknownSubcommandAction rootCmd.AddCommand( - newCreateCommand(), + container.NewCreateCommand(), // #region Run & Exec - newRunCommand(), - newUpdateCommand(), - newExecCommand(), + container.NewRunCommand(), + container.NewUpdateCommand(), + container.NewExecCommand(), // #endregion // #region Container management - newPsCommand(), - newLogsCommand(), - newPortCommand(), - newStopCommand(), - newStartCommand(), - newRestartCommand(), - newKillCommand(), - newRmCommand(), - newPauseCommand(), - newUnpauseCommand(), - newCommitCommand(), - newWaitCommand(), - newRenameCommand(), + container.NewPsCommandForMain(), + container.NewLogsCommand(), + container.NewPortCommand(), + container.NewStopCommand(), + container.NewStartCommand(), + container.NewRestartCommand(), + container.NewKillCommand(), + container.NewRmCommand(), + container.NewPauseCommand(), + container.NewUnpauseCommand(), + container.NewCommitCommand(), + container.NewWaitCommand(), + container.NewRenameCommand(), // #endregion // Build - newBuildCommand(), + builder.NewBuildCommand(), // #region Image management - newImagesCommand(), - newPullCommand(), - newPushCommand(), - newLoadCommand(), - newSaveCommand(), - newTagCommand(), - newRmiCommand(), - newHistoryCommand(), + image.NewImagesCommandForMain(), + image.NewPullCommand(), + image.NewPushCommand(), + image.NewLoadCommand(), + image.NewSaveCommand(), + image.NewTagCommand(), + image.NewRmiCommandForMain(), + image.NewHistoryCommand(), // #endregion // #region System - newEventsCommand(), - newInfoCommand(), - newVersionCommand(), + system.NewEventsCommand(), + system.NewInfoCommand(), + version2.NewVersionCommand(), // #endregion // Inspect - newInspectCommand(), + inspect.NewInspectCommand(), // stats - newTopCommand(), - newStatsCommand(), + container.NewTopCommand(), + container.NewStatsCommand(), // #region Management - newContainerCommand(), - newImageCommand(), - newNetworkCommand(), - newVolumeCommand(), - newSystemCommand(), - newNamespaceCommand(), - newBuilderCommand(), + container.NewContainerCommand(), + image.NewImageCommand(), + network.NewNetworkCommand(), + volume.NewVolumeCommand(), + system.NewSystemCommand(), + namespace.NewNamespaceCommand(), + builder.NewBuilderCommand(), // #endregion // Internal - newInternalCommand(), + internal.NewInternalCommand(), // login - newLoginCommand(), + login.NewLoginCommand(), // Logout - newLogoutCommand(), + logout.NewLogoutCommand(), // Compose - newComposeCommand(), + compose.NewComposeCommand(), // IPFS - newIPFSCommand(), + ipfs.NewIPFSCommand(), ) - addApparmorCommand(rootCmd) - addCpCommand(rootCmd) + apparmor.AddApparmorCommand(rootCmd) + container.AddCpCommand(rootCmd) // add aliasToBeInherited to subCommand(s) InheritedFlags for _, subCmd := range rootCmd.Commands() { @@ -350,243 +362,3 @@ Config file ($NERDCTL_TOML): %s } return rootCmd, nil } - -func globalFlags(cmd *cobra.Command) (string, []string) { - args0, err := os.Executable() - if err != nil { - logrus.WithError(err).Warnf("cannot call os.Executable(), assuming the executable to be %q", os.Args[0]) - args0 = os.Args[0] - } - if len(os.Args) < 2 { - return args0, nil - } - - rootCmd := cmd.Root() - flagSet := rootCmd.Flags() - args := []string{} - flagSet.VisitAll(func(f *pflag.Flag) { - key := f.Name - val := f.Value.String() - if f.Changed { - args = append(args, "--"+key+"="+val) - } - }) - return args0, args -} - -type ExitCoder interface { - error - ExitCode() int -} - -type ExitCodeError struct { - error - exitCode int -} - -func (e ExitCodeError) ExitCode() int { - return e.exitCode -} - -func HandleExitCoder(err error) { - if err == nil { - return - } - - if exitErr, ok := err.(ExitCoder); ok { - os.Exit(exitErr.ExitCode()) - } -} - -// unknownSubcommandAction is needed to let `nerdctl system non-existent-command` fail -// https://github.com/containerd/nerdctl/issues/487 -// -// Ideally this should be implemented in Cobra itself. -func unknownSubcommandAction(cmd *cobra.Command, args []string) error { - if len(args) == 0 { - return cmd.Help() - } - // The output mimics https://github.com/spf13/cobra/blob/v1.2.1/command.go#L647-L662 - msg := fmt.Sprintf("unknown subcommand %q for %q", args[0], cmd.Name()) - if suggestions := cmd.SuggestionsFor(args[0]); len(suggestions) > 0 { - msg += "\n\nDid you mean this?\n" - for _, s := range suggestions { - msg += fmt.Sprintf("\t%v\n", s) - } - } - return errors.New(msg) -} - -// AddStringFlag is similar to cmd.Flags().String but supports aliases and env var -func AddStringFlag(cmd *cobra.Command, name string, aliases []string, value string, env, usage string) { - if env != "" { - usage = fmt.Sprintf("%s [$%s]", usage, env) - } - if envV, ok := os.LookupEnv(env); ok { - value = envV - } - aliasesUsage := fmt.Sprintf("Alias of --%s", name) - p := new(string) - flags := cmd.Flags() - flags.StringVar(p, name, value, usage) - for _, a := range aliases { - if len(a) == 1 { - // pflag doesn't support short-only flags, so we have to register long one as well here - flags.StringVarP(p, a, a, value, aliasesUsage) - } else { - flags.StringVar(p, a, value, aliasesUsage) - } - } -} - -// AddPersistentStringFlag is similar to AddStringFlag but persistent. -// See https://github.com/spf13/cobra/blob/main/user_guide.md#persistent-flags to learn what is "persistent". -func AddPersistentStringFlag(cmd *cobra.Command, name string, aliases, localAliases, persistentAliases []string, aliasToBeInherited *pflag.FlagSet, value string, env, usage string) { - if env != "" { - usage = fmt.Sprintf("%s [$%s]", usage, env) - } - if envV, ok := os.LookupEnv(env); ok { - value = envV - } - aliasesUsage := fmt.Sprintf("Alias of --%s", name) - p := new(string) - - // flags is full set of flag(s) - // flags can redefine alias already used in subcommands - flags := cmd.Flags() - for _, a := range aliases { - if len(a) == 1 { - // pflag doesn't support short-only flags, so we have to register long one as well here - flags.StringVarP(p, a, a, value, aliasesUsage) - } else { - flags.StringVar(p, a, value, aliasesUsage) - } - // non-persistent flags are not added to the InheritedFlags, so we should add them manually - f := flags.Lookup(a) - aliasToBeInherited.AddFlag(f) - } - - // localFlags are local to the rootCmd - localFlags := cmd.LocalFlags() - for _, a := range localAliases { - if len(a) == 1 { - // pflag doesn't support short-only flags, so we have to register long one as well here - localFlags.StringVarP(p, a, a, value, aliasesUsage) - } else { - localFlags.StringVar(p, a, value, aliasesUsage) - } - } - - // persistentFlags cannot redefine alias already used in subcommands - persistentFlags := cmd.PersistentFlags() - persistentFlags.StringVar(p, name, value, usage) - for _, a := range persistentAliases { - if len(a) == 1 { - // pflag doesn't support short-only flags, so we have to register long one as well here - persistentFlags.StringVarP(p, a, a, value, aliasesUsage) - } else { - persistentFlags.StringVar(p, a, value, aliasesUsage) - } - } -} - -// AddPersistentBoolFlag is similar to AddBoolFlag but persistent. -// See https://github.com/spf13/cobra/blob/main/user_guide.md#persistent-flags to learn what is "persistent". -func AddPersistentBoolFlag(cmd *cobra.Command, name string, aliases, nonPersistentAliases []string, value bool, env, usage string) { - if env != "" { - usage = fmt.Sprintf("%s [$%s]", usage, env) - } - if envV, ok := os.LookupEnv(env); ok { - var err error - value, err = strconv.ParseBool(envV) - if err != nil { - logrus.WithError(err).Warnf("Invalid boolean value for `%s`", env) - } - } - aliasesUsage := fmt.Sprintf("Alias of --%s", name) - p := new(bool) - flags := cmd.Flags() - for _, a := range nonPersistentAliases { - if len(a) == 1 { - // pflag doesn't support short-only flags, so we have to register long one as well here - flags.BoolVarP(p, a, a, value, aliasesUsage) - } else { - flags.BoolVar(p, a, value, aliasesUsage) - } - } - - persistentFlags := cmd.PersistentFlags() - persistentFlags.BoolVar(p, name, value, usage) - for _, a := range aliases { - if len(a) == 1 { - // pflag doesn't support short-only flags, so we have to register long one as well here - persistentFlags.BoolVarP(p, a, a, value, aliasesUsage) - } else { - persistentFlags.BoolVar(p, a, value, aliasesUsage) - } - } -} - -// AddPersistentStringArrayFlag is similar to cmd.Flags().StringArray but supports aliases and env var and persistent. -// See https://github.com/spf13/cobra/blob/main/user_guide.md#persistent-flags to learn what is "persistent". -func AddPersistentStringArrayFlag(cmd *cobra.Command, name string, aliases, nonPersistentAliases []string, value []string, env string, usage string) { - if env != "" { - usage = fmt.Sprintf("%s [$%s]", usage, env) - } - if envV, ok := os.LookupEnv(env); ok { - value = []string{envV} - } - aliasesUsage := fmt.Sprintf("Alias of --%s", name) - p := new([]string) - flags := cmd.Flags() - for _, a := range nonPersistentAliases { - if len(a) == 1 { - // pflag doesn't support short-only flags, so we have to register long one as well here - flags.StringArrayVarP(p, a, a, value, aliasesUsage) - } else { - flags.StringArrayVar(p, a, value, aliasesUsage) - } - } - - persistentFlags := cmd.PersistentFlags() - persistentFlags.StringArrayVar(p, name, value, usage) - for _, a := range aliases { - if len(a) == 1 { - // pflag doesn't support short-only flags, so we have to register long one as well here - persistentFlags.StringArrayVarP(p, a, a, value, aliasesUsage) - } else { - persistentFlags.StringArrayVar(p, a, value, aliasesUsage) - } - } -} - -func checkExperimental(feature string) func(cmd *cobra.Command, args []string) error { - return func(cmd *cobra.Command, args []string) error { - experimental, err := cmd.Flags().GetBool("experimental") - if err != nil { - return err - } - if !experimental { - return fmt.Errorf("%s is experimental feature, you should enable experimental config", feature) - } - return nil - } -} - -// IsExactArgs returns an error if there is not the exact number of args -func IsExactArgs(number int) cobra.PositionalArgs { - return func(cmd *cobra.Command, args []string) error { - if len(args) == number { - return nil - } - return fmt.Errorf( - "%q requires exactly %d %s.\nSee '%s --help'.\n\nUsage: %s\n\n%s", - cmd.CommandPath(), - number, - "argument(s)", - cmd.CommandPath(), - cmd.UseLine(), - cmd.Short, - ) - } -} diff --git a/cmd/nerdctl/main_freebsd.go b/cmd/nerdctl/main_freebsd.go index 30ed7a9c1ea..9cb97aaf29e 100644 --- a/cmd/nerdctl/main_freebsd.go +++ b/cmd/nerdctl/main_freebsd.go @@ -23,15 +23,3 @@ import ( func appNeedsRootlessParentMain(cmd *cobra.Command, args []string) bool { return false } - -func shellCompleteCgroupManagerNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return nil, cobra.ShellCompDirectiveNoFileComp -} - -func addApparmorCommand(rootCmd *cobra.Command) { - // NOP -} - -func addCpCommand(rootCmd *cobra.Command) { - // NOP -} diff --git a/cmd/nerdctl/main_linux.go b/cmd/nerdctl/main_linux.go index a20cb00dbb6..bf01eba808c 100644 --- a/cmd/nerdctl/main_linux.go +++ b/cmd/nerdctl/main_linux.go @@ -17,7 +17,6 @@ package main import ( - ncdefaults "github.com/containerd/nerdctl/pkg/defaults" "github.com/containerd/nerdctl/pkg/rootlessutil" "github.com/containerd/nerdctl/pkg/strutil" "github.com/spf13/cobra" @@ -53,22 +52,3 @@ func appNeedsRootlessParentMain(cmd *cobra.Command, args []string) bool { } return true } - -func shellCompleteCgroupManagerNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - candidates := []string{"cgroupfs"} - if ncdefaults.IsSystemdAvailable() { - candidates = append(candidates, "systemd") - } - if rootlessutil.IsRootless() { - candidates = append(candidates, "none") - } - return candidates, cobra.ShellCompDirectiveNoFileComp -} - -func addApparmorCommand(rootCmd *cobra.Command) { - rootCmd.AddCommand(newApparmorCommand()) -} - -func addCpCommand(rootCmd *cobra.Command) { - rootCmd.AddCommand(newCpCommand()) -} diff --git a/cmd/nerdctl/main_unix.go b/cmd/nerdctl/main_unix.go index 3fe1f41105a..a6cad93d6d7 100644 --- a/cmd/nerdctl/main_unix.go +++ b/cmd/nerdctl/main_unix.go @@ -17,51 +17,3 @@ */ package main - -import ( - "github.com/containerd/nerdctl/pkg/infoutil" - "github.com/containerd/nerdctl/pkg/rootlessutil" - "github.com/sirupsen/logrus" - "github.com/spf13/cobra" -) - -func shellCompleteNamespaceNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if rootlessutil.IsRootlessParent() { - _ = rootlessutil.ParentMain() - return nil, cobra.ShellCompDirectiveNoFileComp - } - - client, ctx, cancel, err := newClient(cmd) - if err != nil { - return nil, cobra.ShellCompDirectiveError - } - defer cancel() - nsService := client.NamespaceService() - nsList, err := nsService.List(ctx) - if err != nil { - logrus.Warn(err) - return nil, cobra.ShellCompDirectiveError - } - var candidates []string - candidates = append(candidates, nsList...) - return candidates, cobra.ShellCompDirectiveNoFileComp -} - -func shellCompleteSnapshotterNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if rootlessutil.IsRootlessParent() { - _ = rootlessutil.ParentMain() - return nil, cobra.ShellCompDirectiveNoFileComp - } - client, ctx, cancel, err := newClient(cmd) - if err != nil { - return nil, cobra.ShellCompDirectiveError - } - defer cancel() - snapshotterPlugins, err := infoutil.GetSnapshotterNames(ctx, client.IntrospectionService()) - if err != nil { - return nil, cobra.ShellCompDirectiveError - } - var candidates []string - candidates = append(candidates, snapshotterPlugins...) - return candidates, cobra.ShellCompDirectiveNoFileComp -} diff --git a/cmd/nerdctl/main_windows.go b/cmd/nerdctl/main_windows.go index e8c48782a20..9cb97aaf29e 100644 --- a/cmd/nerdctl/main_windows.go +++ b/cmd/nerdctl/main_windows.go @@ -23,23 +23,3 @@ import ( func appNeedsRootlessParentMain(cmd *cobra.Command, args []string) bool { return false } - -func shellCompleteNamespaceNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return nil, cobra.ShellCompDirectiveNoFileComp -} - -func shellCompleteSnapshotterNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return nil, cobra.ShellCompDirectiveNoFileComp -} - -func shellCompleteCgroupManagerNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return nil, cobra.ShellCompDirectiveNoFileComp -} - -func addApparmorCommand(rootCmd *cobra.Command) { - // NOP -} - -func addCpCommand(rootCmd *cobra.Command) { - // NOP -} diff --git a/cmd/nerdctl/namespace.go b/cmd/nerdctl/namespace/namespace.go similarity index 80% rename from cmd/nerdctl/namespace.go rename to cmd/nerdctl/namespace/namespace.go index 0d05926c69e..d43c0e22cae 100644 --- a/cmd/nerdctl/namespace.go +++ b/cmd/nerdctl/namespace/namespace.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package namespace import ( "fmt" @@ -24,31 +24,34 @@ import ( "text/tabwriter" "github.com/containerd/containerd/namespaces" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/common" "github.com/containerd/nerdctl/pkg/mountutil/volumestore" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) -func newNamespaceCommand() *cobra.Command { +func NewNamespaceCommand() *cobra.Command { namespaceCommand := &cobra.Command{ - Annotations: map[string]string{Category: Management}, + Annotations: map[string]string{common.Category: common.Management}, Use: "namespace", Aliases: []string{"ns"}, Short: "Manage containerd namespaces", Long: "Unrelated to Linux namespaces and Kubernetes namespaces", - RunE: unknownSubcommandAction, + RunE: completion.UnknownSubcommandAction, SilenceUsage: true, SilenceErrors: true, } - namespaceCommand.AddCommand(newNamespaceLsCommand()) - namespaceCommand.AddCommand(newNamespaceRmCommand()) - namespaceCommand.AddCommand(newNamespaceCreateCommand()) - namespaceCommand.AddCommand(newNamespacelabelUpdateCommand()) - namespaceCommand.AddCommand(newNamespaceInspectCommand()) + namespaceCommand.AddCommand(NewLsCommand()) + namespaceCommand.AddCommand(NewRmCommand()) + namespaceCommand.AddCommand(NewCreateCommand()) + namespaceCommand.AddCommand(NewUpdateCommand()) + namespaceCommand.AddCommand(NewInspectCommand()) return namespaceCommand } -func newNamespaceLsCommand() *cobra.Command { +func NewLsCommand() *cobra.Command { namespaceLsCommand := &cobra.Command{ Use: "ls", Aliases: []string{"list"}, @@ -62,7 +65,7 @@ func newNamespaceLsCommand() *cobra.Command { } func namespaceLsAction(cmd *cobra.Command, args []string) error { - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } @@ -84,7 +87,7 @@ func namespaceLsAction(cmd *cobra.Command, args []string) error { return nil } - dataStore, err := getDataStore(cmd) + dataStore, err := ncclient.DataStore(cmd) if err != nil { return err } diff --git a/cmd/nerdctl/namespace_create.go b/cmd/nerdctl/namespace/namespace_create.go similarity index 90% rename from cmd/nerdctl/namespace_create.go rename to cmd/nerdctl/namespace/namespace_create.go index 481f41e6c69..80ad04bd436 100644 --- a/cmd/nerdctl/namespace_create.go +++ b/cmd/nerdctl/namespace/namespace_create.go @@ -14,14 +14,15 @@ limitations under the License. */ -package main +package namespace import ( "github.com/containerd/containerd/cmd/ctr/commands" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" "github.com/spf13/cobra" ) -func newNamespaceCreateCommand() *cobra.Command { +func NewCreateCommand() *cobra.Command { namespaceCreateCommand := &cobra.Command{ Use: "create NAMESPACE", Short: "Create a new namespace", @@ -41,7 +42,7 @@ func namespaceCreateAction(cmd *cobra.Command, args []string) error { return err } - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } diff --git a/cmd/nerdctl/namespace_freebsd.go b/cmd/nerdctl/namespace/namespace_freebsd.go similarity index 97% rename from cmd/nerdctl/namespace_freebsd.go rename to cmd/nerdctl/namespace/namespace_freebsd.go index 688ea349d77..d13c19c6f9e 100644 --- a/cmd/nerdctl/namespace_freebsd.go +++ b/cmd/nerdctl/namespace/namespace_freebsd.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package namespace import ( "github.com/containerd/containerd/namespaces" diff --git a/cmd/nerdctl/namespace_inspect.go b/cmd/nerdctl/namespace/namespace_inspect.go similarity index 87% rename from cmd/nerdctl/namespace_inspect.go rename to cmd/nerdctl/namespace/namespace_inspect.go index 408164082bb..3d16452df8e 100644 --- a/cmd/nerdctl/namespace_inspect.go +++ b/cmd/nerdctl/namespace/namespace_inspect.go @@ -14,15 +14,17 @@ limitations under the License. */ -package main +package namespace import ( "github.com/containerd/containerd/namespaces" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/fmtutil" "github.com/containerd/nerdctl/pkg/inspecttypes/native" "github.com/spf13/cobra" ) -func newNamespaceInspectCommand() *cobra.Command { +func NewInspectCommand() *cobra.Command { namespaceInspectCommand := &cobra.Command{ Use: "inspect NAMESPACE", Short: "Display detailed information on one or more namespaces.", @@ -39,7 +41,7 @@ func newNamespaceInspectCommand() *cobra.Command { } func labelInspectAction(cmd *cobra.Command, args []string) error { - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } @@ -58,5 +60,5 @@ func labelInspectAction(cmd *cobra.Command, args []string) error { } result[index] = nsInspect } - return formatSlice(cmd, result) + return fmtutil.FormatSlice(cmd, result) } diff --git a/cmd/nerdctl/namespace_linux.go b/cmd/nerdctl/namespace/namespace_linux.go similarity index 98% rename from cmd/nerdctl/namespace_linux.go rename to cmd/nerdctl/namespace/namespace_linux.go index 46f2c1958bd..da9852418b9 100644 --- a/cmd/nerdctl/namespace_linux.go +++ b/cmd/nerdctl/namespace/namespace_linux.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package namespace import ( "github.com/containerd/containerd/namespaces" diff --git a/cmd/nerdctl/namespace_rm.go b/cmd/nerdctl/namespace/namespace_rm.go similarity index 91% rename from cmd/nerdctl/namespace_rm.go rename to cmd/nerdctl/namespace/namespace_rm.go index 68bfeaa913b..a8e919e6383 100644 --- a/cmd/nerdctl/namespace_rm.go +++ b/cmd/nerdctl/namespace/namespace_rm.go @@ -14,17 +14,18 @@ limitations under the License. */ -package main +package namespace import ( "fmt" "github.com/containerd/containerd/errdefs" "github.com/containerd/containerd/log" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" "github.com/spf13/cobra" ) -func newNamespaceRmCommand() *cobra.Command { +func NewRmCommand() *cobra.Command { namespaceRmCommand := &cobra.Command{ Use: "remove [flags] NAMESPACE [NAMESPACE...]", Aliases: []string{"rm"}, @@ -40,7 +41,7 @@ func newNamespaceRmCommand() *cobra.Command { func namespaceRmAction(cmd *cobra.Command, args []string) error { var exitErr error - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } diff --git a/cmd/nerdctl/namespace_update.go b/cmd/nerdctl/namespace/namespace_update.go similarity index 89% rename from cmd/nerdctl/namespace_update.go rename to cmd/nerdctl/namespace/namespace_update.go index b5127e28bfb..fefcc5bf69c 100644 --- a/cmd/nerdctl/namespace_update.go +++ b/cmd/nerdctl/namespace/namespace_update.go @@ -14,13 +14,14 @@ limitations under the License. */ -package main +package namespace import ( + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" "github.com/spf13/cobra" ) -func newNamespacelabelUpdateCommand() *cobra.Command { +func NewUpdateCommand() *cobra.Command { namespaceLableCommand := &cobra.Command{ Use: "update [flags] NAMESPACE", Short: "Update labels for a namespace", @@ -39,7 +40,7 @@ func labelUpdateAction(cmd *cobra.Command, args []string) error { return err } - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } diff --git a/cmd/nerdctl/namespace_windows.go b/cmd/nerdctl/namespace/namespace_windows.go similarity index 97% rename from cmd/nerdctl/namespace_windows.go rename to cmd/nerdctl/namespace/namespace_windows.go index 688ea349d77..d13c19c6f9e 100644 --- a/cmd/nerdctl/namespace_windows.go +++ b/cmd/nerdctl/namespace/namespace_windows.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package namespace import ( "github.com/containerd/containerd/namespaces" diff --git a/cmd/nerdctl/network.go b/cmd/nerdctl/network/network.go similarity index 67% rename from cmd/nerdctl/network.go rename to cmd/nerdctl/network/network.go index cc9b38d5f51..e1b24286200 100644 --- a/cmd/nerdctl/network.go +++ b/cmd/nerdctl/network/network.go @@ -14,27 +14,29 @@ limitations under the License. */ -package main +package network import ( + "github.com/containerd/nerdctl/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/common" "github.com/spf13/cobra" ) -func newNetworkCommand() *cobra.Command { +func NewNetworkCommand() *cobra.Command { networkCommand := &cobra.Command{ - Annotations: map[string]string{Category: Management}, + Annotations: map[string]string{common.Category: common.Management}, Use: "network", Short: "Manage networks", - RunE: unknownSubcommandAction, + RunE: completion.UnknownSubcommandAction, SilenceUsage: true, SilenceErrors: true, } networkCommand.AddCommand( - newNetworkLsCommand(), - newNetworkInspectCommand(), - newNetworkCreateCommand(), - newNetworkRmCommand(), - newNetworkPruneCommand(), + NewLsCommand(), + NewInspectCommand(), + NewCreateCommand(), + NewRmCommand(), + NewPruneCommand(), ) return networkCommand } diff --git a/cmd/nerdctl/network_create.go b/cmd/nerdctl/network/network_create.go similarity index 96% rename from cmd/nerdctl/network_create.go rename to cmd/nerdctl/network/network_create.go index 9d7328fc3f6..dcda2859440 100644 --- a/cmd/nerdctl/network_create.go +++ b/cmd/nerdctl/network/network_create.go @@ -14,25 +14,26 @@ limitations under the License. */ -package main +package network import ( "fmt" "github.com/containerd/containerd/errdefs" "github.com/containerd/containerd/identifiers" + "github.com/containerd/nerdctl/cmd/nerdctl/utils" "github.com/containerd/nerdctl/pkg/netutil" "github.com/containerd/nerdctl/pkg/strutil" "github.com/spf13/cobra" ) -func newNetworkCreateCommand() *cobra.Command { +func NewCreateCommand() *cobra.Command { var networkCreateCommand = &cobra.Command{ Use: "create [flags] NETWORK", Short: "Create a network", Long: `NOTE: To isolate CNI bridge, CNI plugin "firewall" (>= v1.1.0) is needed.`, - Args: IsExactArgs(1), + Args: utils.IsExactArgs(1), RunE: networkCreateAction, SilenceUsage: true, SilenceErrors: true, diff --git a/cmd/nerdctl/network_create_unix.go b/cmd/nerdctl/network/network_create_unix.go similarity index 98% rename from cmd/nerdctl/network_create_unix.go rename to cmd/nerdctl/network/network_create_unix.go index 298030ba5bc..b2c8c062f33 100644 --- a/cmd/nerdctl/network_create_unix.go +++ b/cmd/nerdctl/network/network_create_unix.go @@ -16,7 +16,7 @@ limitations under the License. */ -package main +package network import "github.com/spf13/cobra" diff --git a/cmd/nerdctl/network_create_windows.go b/cmd/nerdctl/network/network_create_windows.go similarity index 98% rename from cmd/nerdctl/network_create_windows.go rename to cmd/nerdctl/network/network_create_windows.go index 2d6acf7e92b..3cb69b7a938 100644 --- a/cmd/nerdctl/network_create_windows.go +++ b/cmd/nerdctl/network/network_create_windows.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package network import "github.com/spf13/cobra" diff --git a/cmd/nerdctl/network_inspect.go b/cmd/nerdctl/network/network_inspect.go similarity index 92% rename from cmd/nerdctl/network_inspect.go rename to cmd/nerdctl/network/network_inspect.go index 06facf36d93..4df86c2fb59 100644 --- a/cmd/nerdctl/network_inspect.go +++ b/cmd/nerdctl/network/network_inspect.go @@ -14,12 +14,14 @@ limitations under the License. */ -package main +package network import ( "encoding/json" "fmt" + "github.com/containerd/nerdctl/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/fmtutil" "github.com/containerd/nerdctl/pkg/inspecttypes/dockercompat" "github.com/containerd/nerdctl/pkg/inspecttypes/native" "github.com/containerd/nerdctl/pkg/netutil" @@ -27,7 +29,7 @@ import ( "github.com/spf13/cobra" ) -func newNetworkInspectCommand() *cobra.Command { +func NewInspectCommand() *cobra.Command { networkInspectCommand := &cobra.Command{ Use: "inspect [flags] NETWORK [NETWORK, ...]", Short: "Display detailed information on one or more networks", @@ -101,11 +103,11 @@ func networkInspectAction(cmd *cobra.Command, args []string) error { } } - return formatSlice(cmd, result) + return fmtutil.FormatSlice(cmd, result) } func networkInspectShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // show network names, including "bridge" exclude := []string{"host", "none"} - return shellCompleteNetworkNames(cmd, exclude) + return completion.ShellCompleteNetworkNames(cmd, exclude) } diff --git a/cmd/nerdctl/network_ls.go b/cmd/nerdctl/network/network_ls.go similarity index 93% rename from cmd/nerdctl/network_ls.go rename to cmd/nerdctl/network/network_ls.go index 26933333f33..388f3f3fdc1 100644 --- a/cmd/nerdctl/network_ls.go +++ b/cmd/nerdctl/network/network_ls.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package network import ( "bytes" @@ -23,12 +23,13 @@ import ( "text/tabwriter" "text/template" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/fmtutil" "github.com/containerd/nerdctl/pkg/netutil" "github.com/spf13/cobra" ) -func newNetworkLsCommand() *cobra.Command { +func NewLsCommand() *cobra.Command { cmd := &cobra.Command{ Use: "ls", Aliases: []string{"list"}, @@ -79,7 +80,7 @@ func networkLsAction(cmd *cobra.Command, args []string) error { return errors.New("format and quiet must not be specified together") } var err error - tmpl, err = parseTemplate(format) + tmpl, err = fmtutil.ParseTemplate(format) if err != nil { return err } @@ -114,7 +115,7 @@ func networkLsAction(cmd *cobra.Command, args []string) error { } } if n.NerdctlLabels != nil { - p.Labels = formatLabels(*n.NerdctlLabels) + p.Labels = fmtutil.FormatLabels(*n.NerdctlLabels) } pp[i] = p } @@ -146,7 +147,7 @@ func networkLsAction(cmd *cobra.Command, args []string) error { fmt.Fprintf(w, "%s\t%s\t%s\n", p.ID, p.Name, p.file) } } - if f, ok := w.(Flusher); ok { + if f, ok := w.(fmtutil.Flusher); ok { return f.Flush() } return nil diff --git a/cmd/nerdctl/network_prune.go b/cmd/nerdctl/network/network_prune.go similarity index 91% rename from cmd/nerdctl/network_prune.go rename to cmd/nerdctl/network/network_prune.go index 30426d9f22d..adf21719142 100644 --- a/cmd/nerdctl/network_prune.go +++ b/cmd/nerdctl/network/network_prune.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package network import ( "context" @@ -22,6 +22,7 @@ import ( "strings" "github.com/containerd/containerd" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" "github.com/containerd/nerdctl/pkg/netutil" "github.com/containerd/nerdctl/pkg/strutil" "github.com/sirupsen/logrus" @@ -30,7 +31,7 @@ import ( var networkDriversToKeep = []string{"host", "none", DefaultNetworkDriver} -func newNetworkPruneCommand() *cobra.Command { +func NewPruneCommand() *cobra.Command { networkPruneCommand := &cobra.Command{ Use: "prune [flags]", Short: "Remove all unused networks", @@ -62,16 +63,16 @@ func networkPruneAction(cmd *cobra.Command, _ []string) error { } } - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } defer cancel() - return networkPrune(ctx, cmd, client) + return Prune(ctx, cmd, client) } -func networkPrune(ctx context.Context, cmd *cobra.Command, client *containerd.Client) error { +func Prune(ctx context.Context, cmd *cobra.Command, client *containerd.Client) error { cniPath, err := cmd.Flags().GetString("cni-path") if err != nil { return err diff --git a/cmd/nerdctl/network_rm.go b/cmd/nerdctl/network/network_rm.go similarity index 89% rename from cmd/nerdctl/network_rm.go rename to cmd/nerdctl/network/network_rm.go index 41beb2d668b..eab9ac83db5 100644 --- a/cmd/nerdctl/network_rm.go +++ b/cmd/nerdctl/network/network_rm.go @@ -14,12 +14,15 @@ limitations under the License. */ -package main +package network import ( "context" "fmt" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/common" "github.com/containerd/nerdctl/pkg/idutil/netwalker" "github.com/containerd/nerdctl/pkg/netutil" "github.com/sirupsen/logrus" @@ -27,7 +30,7 @@ import ( "github.com/spf13/cobra" ) -func newNetworkRmCommand() *cobra.Command { +func NewRmCommand() *cobra.Command { networkRmCommand := &cobra.Command{ Use: "rm [flags] NETWORK [NETWORK, ...]", Aliases: []string{"remove"}, @@ -43,7 +46,7 @@ func newNetworkRmCommand() *cobra.Command { } func networkRmAction(cmd *cobra.Command, args []string) error { - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } @@ -113,8 +116,8 @@ func networkRmAction(cmd *cobra.Command, args []string) error { // compatible with docker // ExitCodeError is to allow the program to exit with status code 1 without outputting an error message. if code != 0 { - return ExitCodeError{ - exitCode: code, + return common.ExitCodeError{ + Code: code, } } return nil @@ -123,5 +126,5 @@ func networkRmAction(cmd *cobra.Command, args []string) error { func networkRmShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // show network names, including "bridge" exclude := []string{netutil.DefaultNetworkName, "host", "none"} - return shellCompleteNetworkNames(cmd, exclude) + return completion.ShellCompleteNetworkNames(cmd, exclude) } diff --git a/cmd/nerdctl/run.go b/cmd/nerdctl/run.go deleted file mode 100644 index 20aa7761b73..00000000000 --- a/cmd/nerdctl/run.go +++ /dev/null @@ -1,1274 +0,0 @@ -/* - 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 main - -import ( - "bufio" - "context" - "encoding/json" - "errors" - "fmt" - "net/url" - "os" - "os/exec" - "path" - "path/filepath" - "runtime" - "strconv" - "strings" - - "github.com/containerd/console" - "github.com/containerd/containerd" - "github.com/containerd/containerd/cio" - "github.com/containerd/containerd/cmd/ctr/commands" - "github.com/containerd/containerd/cmd/ctr/commands/tasks" - "github.com/containerd/containerd/containers" - "github.com/containerd/containerd/oci" - gocni "github.com/containerd/go-cni" - "github.com/containerd/nerdctl/pkg/defaults" - "github.com/containerd/nerdctl/pkg/idgen" - "github.com/containerd/nerdctl/pkg/imgutil" - "github.com/containerd/nerdctl/pkg/inspecttypes/dockercompat" - "github.com/containerd/nerdctl/pkg/labels" - "github.com/containerd/nerdctl/pkg/logging" - "github.com/containerd/nerdctl/pkg/mountutil" - "github.com/containerd/nerdctl/pkg/namestore" - "github.com/containerd/nerdctl/pkg/netutil" - "github.com/containerd/nerdctl/pkg/platformutil" - "github.com/containerd/nerdctl/pkg/referenceutil" - "github.com/containerd/nerdctl/pkg/rootlessutil" - "github.com/containerd/nerdctl/pkg/strutil" - "github.com/containerd/nerdctl/pkg/taskutil" - dopts "github.com/docker/cli/opts" - "github.com/opencontainers/runtime-spec/specs-go" - "github.com/sirupsen/logrus" - "github.com/spf13/cobra" -) - -const ( - tiniInitBinary = "tini" -) - -func newRunCommand() *cobra.Command { - shortHelp := "Run a command in a new container. Optionally specify \"ipfs://\" or \"ipns://\" scheme to pull image from IPFS." - longHelp := shortHelp - switch runtime.GOOS { - case "windows": - longHelp += "\n" - longHelp += "WARNING: `nerdctl run` is experimental on Windows and currently broken (https://github.com/containerd/nerdctl/issues/28)" - case "freebsd": - longHelp += "\n" - longHelp += "WARNING: `nerdctl run` is experimental on FreeBSD and currently requires `--net=none` (https://github.com/containerd/nerdctl/blob/main/docs/freebsd.md)" - } - var runCommand = &cobra.Command{ - Use: "run [flags] IMAGE [COMMAND] [ARG...]", - Args: cobra.MinimumNArgs(1), - Short: shortHelp, - Long: longHelp, - RunE: runAction, - ValidArgsFunction: runShellComplete, - SilenceUsage: true, - SilenceErrors: true, - } - - runCommand.Flags().SetInterspersed(false) - setCreateFlags(runCommand) - - runCommand.Flags().BoolP("detach", "d", false, "Run container in background and print container ID") - - return runCommand -} - -func setCreateFlags(cmd *cobra.Command) { - - // No "-h" alias for "--help", because "-h" for "--hostname". - cmd.Flags().Bool("help", false, "show help") - - cmd.Flags().BoolP("tty", "t", false, "(Currently -t needs to correspond to -i)") - cmd.Flags().BoolP("interactive", "i", false, "Keep STDIN open even if not attached") - cmd.Flags().String("restart", "no", `Restart policy to apply when a container exits (implemented values: "no"|"always")`) - cmd.RegisterFlagCompletionFunc("restart", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return []string{"no", "always", "on-failure", "unless-stopped"}, cobra.ShellCompDirectiveNoFileComp - }) - cmd.Flags().Bool("rm", false, "Automatically remove the container when it exits") - cmd.Flags().String("pull", "missing", `Pull image before running ("always"|"missing"|"never")`) - cmd.RegisterFlagCompletionFunc("pull", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return []string{"always", "missing", "never"}, cobra.ShellCompDirectiveNoFileComp - }) - cmd.Flags().String("stop-signal", "SIGTERM", "Signal to stop a container") - cmd.Flags().Int("stop-timeout", 0, "Timeout (in seconds) to stop a container") - - // #region for init process - cmd.Flags().Bool("init", false, "Run an init process inside the container, Default to use tini") - cmd.Flags().String("init-binary", tiniInitBinary, "The custom binary to use as the init process") - // #endregion - - // #region platform flags - cmd.Flags().String("platform", "", "Set platform (e.g. \"amd64\", \"arm64\")") // not a slice, and there is no --all-platforms - cmd.RegisterFlagCompletionFunc("platform", shellCompletePlatforms) - // #endregion - - // #region network flags - // network (net) is defined as StringSlice, not StringArray, to allow specifying "--network=cni1,cni2" - cmd.Flags().StringSlice("network", []string{netutil.DefaultNetworkName}, `Connect a container to a network ("bridge"|"host"|"none"|"container:"|)`) - cmd.RegisterFlagCompletionFunc("network", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return shellCompleteNetworkNames(cmd, []string{}) - }) - cmd.Flags().StringSlice("net", []string{netutil.DefaultNetworkName}, `Connect a container to a network ("bridge"|"host"|"none"|)`) - cmd.RegisterFlagCompletionFunc("net", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return shellCompleteNetworkNames(cmd, []string{}) - }) - // dns is defined as StringSlice, not StringArray, to allow specifying "--dns=1.1.1.1,8.8.8.8" (compatible with Podman) - cmd.Flags().StringSlice("dns", nil, "Set custom DNS servers") - cmd.Flags().StringSlice("dns-search", nil, "Set custom DNS search domains") - // We allow for both "--dns-opt" and "--dns-option", although the latter is the recommended way. - cmd.Flags().StringSlice("dns-opt", nil, "Set DNS options") - cmd.Flags().StringSlice("dns-option", nil, "Set DNS options") - // publish is defined as StringSlice, not StringArray, to allow specifying "--publish=80:80,443:443" (compatible with Podman) - cmd.Flags().StringSliceP("publish", "p", nil, "Publish a container's port(s) to the host") - // FIXME: not support IPV6 yet - cmd.Flags().String("ip", "", "IPv4 address to assign to the container") - cmd.Flags().StringP("hostname", "h", "", "Container host name") - cmd.Flags().String("mac-address", "", "MAC address to assign to the container") - // #endregion - - cmd.Flags().String("ipc", "", `IPC namespace to use ("host"|"private")`) - cmd.RegisterFlagCompletionFunc("ipc", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return []string{"host", "private"}, cobra.ShellCompDirectiveNoFileComp - }) - // #region cgroups, namespaces, and ulimits flags - cmd.Flags().Float64("cpus", 0.0, "Number of CPUs") - cmd.Flags().StringP("memory", "m", "", "Memory limit") - cmd.Flags().String("memory-reservation", "", "Memory soft limit") - cmd.Flags().String("memory-swap", "", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap") - cmd.Flags().Int64("memory-swappiness", -1, "Tune container memory swappiness (0 to 100) (default -1)") - cmd.Flags().String("kernel-memory", "", "Kernel memory limit (deprecated)") - cmd.Flags().Bool("oom-kill-disable", false, "Disable OOM Killer") - cmd.Flags().Int("oom-score-adj", 0, "Tune container’s OOM preferences (-1000 to 1000, rootless: 100 to 1000)") - cmd.Flags().String("pid", "", "PID namespace to use") - cmd.RegisterFlagCompletionFunc("pid", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return []string{"host"}, cobra.ShellCompDirectiveNoFileComp - }) - cmd.Flags().Int64("pids-limit", -1, "Tune container pids limit (set -1 for unlimited)") - cmd.Flags().StringSlice("cgroup-conf", nil, "Configure cgroup v2 (key=value)") - cmd.Flags().Uint16("blkio-weight", 0, "Block IO (relative weight), between 10 and 1000, or 0 to disable (default 0)") - cmd.Flags().String("cgroupns", defaults.CgroupnsMode(), `Cgroup namespace to use, the default depends on the cgroup version ("host"|"private")`) - cmd.RegisterFlagCompletionFunc("cgroupns", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return []string{"host", "private"}, cobra.ShellCompDirectiveNoFileComp - }) - cmd.Flags().String("cpuset-cpus", "", "CPUs in which to allow execution (0-3, 0,1)") - cmd.Flags().String("cpuset-mems", "", "MEMs in which to allow execution (0-3, 0,1)") - cmd.Flags().Uint64("cpu-shares", 0, "CPU shares (relative weight)") - cmd.Flags().Int64("cpu-quota", -1, "Limit CPU CFS (Completely Fair Scheduler) quota") - cmd.Flags().Uint64("cpu-period", 0, "Limit CPU CFS (Completely Fair Scheduler) period") - // device is defined as StringSlice, not StringArray, to allow specifying "--device=DEV1,DEV2" (compatible with Podman) - cmd.Flags().StringSlice("device", nil, "Add a host device to the container") - // ulimit is defined as StringSlice, not StringArray, to allow specifying "--ulimit=ULIMIT1,ULIMIT2" (compatible with Podman) - cmd.Flags().StringSlice("ulimit", nil, "Ulimit options") - cmd.Flags().String("rdt-class", "", "Name of the RDT class (or CLOS) to associate the container with") - // #endregion - - // user flags - cmd.Flags().StringP("user", "u", "", "Username or UID (format: [:])") - cmd.Flags().String("umask", "", "Set the umask inside the container. Defaults to 0022") - cmd.Flags().StringSlice("group-add", []string{}, "Add additional groups to join") - - // #region security flags - cmd.Flags().StringArray("security-opt", []string{}, "Security options") - cmd.RegisterFlagCompletionFunc("security-opt", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return []string{"seccomp=", "seccomp=unconfined", "apparmor=", "apparmor=" + defaults.AppArmorProfileName, "apparmor=unconfined", "no-new-privileges", "privileged-without-host-devices"}, cobra.ShellCompDirectiveNoFileComp - }) - // cap-add and cap-drop are defined as StringSlice, not StringArray, to allow specifying "--cap-add=CAP_SYS_ADMIN,CAP_NET_ADMIN" (compatible with Podman) - cmd.Flags().StringSlice("cap-add", []string{}, "Add Linux capabilities") - cmd.RegisterFlagCompletionFunc("cap-add", capShellComplete) - cmd.Flags().StringSlice("cap-drop", []string{}, "Drop Linux capabilities") - cmd.RegisterFlagCompletionFunc("cap-drop", capShellComplete) - cmd.Flags().Bool("privileged", false, "Give extended privileges to this container") - // #endregion - - // #region runtime flags - cmd.Flags().String("runtime", defaults.Runtime, "Runtime to use for this container, e.g. \"crun\", or \"io.containerd.runsc.v1\"") - // sysctl needs to be StringArray, not StringSlice, to prevent "foo=foo1,foo2" from being split to {"foo=foo1", "foo2"} - cmd.Flags().StringArray("sysctl", nil, "Sysctl options") - // gpus needs to be StringArray, not StringSlice, to prevent "capabilities=utility,device=DEV" from being split to {"capabilities=utility", "device=DEV"} - cmd.Flags().StringArray("gpus", nil, "GPU devices to add to the container ('all' to pass all GPUs)") - cmd.RegisterFlagCompletionFunc("gpus", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return []string{"all"}, cobra.ShellCompDirectiveNoFileComp - }) - // #endregion - - // #region mount flags - // volume needs to be StringArray, not StringSlice, to prevent "/foo:/foo:ro,Z" from being split to {"/foo:/foo:ro", "Z"} - cmd.Flags().StringArrayP("volume", "v", nil, "Bind mount a volume") - // tmpfs needs to be StringArray, not StringSlice, to prevent "/foo:size=64m,exec" from being split to {"/foo:size=64m", "exec"} - cmd.Flags().StringArray("tmpfs", nil, "Mount a tmpfs directory") - cmd.Flags().StringArray("mount", nil, "Attach a filesystem mount to the container") - // #endregion - - // rootfs flags - cmd.Flags().Bool("read-only", false, "Mount the container's root filesystem as read only") - // rootfs flags (from Podman) - cmd.Flags().Bool("rootfs", false, "The first argument is not an image but the rootfs to the exploded container") - - // #region env flags - // entrypoint needs to be StringArray, not StringSlice, to prevent "FOO=foo1,foo2" from being split to {"FOO=foo1", "foo2"} - // entrypoint StringArray is an internal implementation to support `nerdctl compose` entrypoint yaml filed with multiple strings - // users are not expected to specify multiple --entrypoint flags manually. - cmd.Flags().StringArray("entrypoint", nil, "Overwrite the default ENTRYPOINT of the image") - cmd.Flags().StringP("workdir", "w", "", "Working directory inside the container") - // env needs to be StringArray, not StringSlice, to prevent "FOO=foo1,foo2" from being split to {"FOO=foo1", "foo2"} - cmd.Flags().StringArrayP("env", "e", nil, "Set environment variables") - // add-host is defined as StringSlice, not StringArray, to allow specifying "--add-host=HOST1:IP1,HOST2:IP2" (compatible with Podman) - cmd.Flags().StringSlice("add-host", nil, "Add a custom host-to-IP mapping (host:ip)") - // env-file is defined as StringSlice, not StringArray, to allow specifying "--env-file=FILE1,FILE2" (compatible with Podman) - cmd.Flags().StringSlice("env-file", nil, "Set environment variables from file") - - // #region metadata flags - cmd.Flags().String("name", "", "Assign a name to the container") - // label needs to be StringArray, not StringSlice, to prevent "foo=foo1,foo2" from being split to {"foo=foo1", "foo2"} - cmd.Flags().StringArrayP("label", "l", nil, "Set metadata on container") - cmd.RegisterFlagCompletionFunc("label", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return labels.ShellCompletions, cobra.ShellCompDirectiveNoFileComp - }) - - // label-file is defined as StringSlice, not StringArray, to allow specifying "--env-file=FILE1,FILE2" (compatible with Podman) - cmd.Flags().StringSlice("label-file", nil, "Set metadata on container from file") - cmd.Flags().String("cidfile", "", "Write the container ID to the file") - // #endregion - - // #region logging flags - // log-opt needs to be StringArray, not StringSlice, to prevent "env=os,customer" from being split to {"env=os", "customer"} - cmd.Flags().String("log-driver", "json-file", "Logging driver for the container. Default is json-file. It also supports logURI (eg: --log-driver binary://)") - cmd.RegisterFlagCompletionFunc("log-driver", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return logging.Drivers(), cobra.ShellCompDirectiveNoFileComp - }) - cmd.Flags().StringArray("log-opt", nil, "Log driver options") - // #endregion - - // shared memory flags - cmd.Flags().String("shm-size", "", "Size of /dev/shm") - cmd.Flags().String("pidfile", "", "file path to write the task's pid") - - // #region verify flags - cmd.Flags().String("verify", "none", "Verify the image (none|cosign)") - cmd.RegisterFlagCompletionFunc("verify", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return []string{"none", "cosign"}, cobra.ShellCompDirectiveNoFileComp - }) - cmd.Flags().String("cosign-key", "", "Path to the public key file, KMS, URI or Kubernetes Secret for --verify=cosign") - // #endregion -} - -// runAction is heavily based on ctr implementation: -// https://github.com/containerd/containerd/blob/v1.4.3/cmd/ctr/commands/run/run.go -func runAction(cmd *cobra.Command, args []string) error { - platform, err := cmd.Flags().GetString("platform") - if err != nil { - return err - } - client, ctx, cancel, err := newClientWithPlatform(cmd, platform) - if err != nil { - return err - } - defer cancel() - - flagD, err := cmd.Flags().GetBool("detach") - if err != nil { - return err - } - flagI, err := cmd.Flags().GetBool("interactive") - if err != nil { - return err - } - flagT, err := cmd.Flags().GetBool("tty") - if err != nil { - return err - } - container, gc, err := createContainer(ctx, cmd, client, args, platform, flagI, flagT, flagD) - if err != nil { - if gc != nil { - defer gc() - } - return err - } - - id := container.ID() - rm, err := cmd.Flags().GetBool("rm") - if err != nil { - return err - } - if rm { - if flagD { - return errors.New("flag -d and --rm cannot be specified together") - } - defer func() { - if err := removeContainer(ctx, cmd, container, true, true); err != nil { - logrus.WithError(err).Warnf("failed to remove container %s", id) - } - }() - } - - var con console.Console - if flagT { - con = console.Current() - defer con.Reset() - if err := con.SetRaw(); err != nil { - return err - } - } - - lab, err := container.Labels(ctx) - if err != nil { - return err - } - logURI := lab[labels.LogURI] - - task, err := taskutil.NewTask(ctx, client, container, flagI, flagT, flagD, con, logURI) - if err != nil { - return err - } - var statusC <-chan containerd.ExitStatus - if !flagD { - defer func() { - if rm { - if _, taskDeleteErr := task.Delete(ctx); taskDeleteErr != nil { - logrus.Error(taskDeleteErr) - } - } - }() - statusC, err = task.Wait(ctx) - if err != nil { - return err - } - } - - if err := task.Start(ctx); err != nil { - return err - } - - if flagD { - fmt.Fprintf(cmd.OutOrStdout(), "%s\n", id) - return nil - } - if flagT { - if err := tasks.HandleConsoleResize(ctx, task, con); err != nil { - logrus.WithError(err).Error("console resize") - } - } else { - sigc := commands.ForwardAllSignals(ctx, task) - defer commands.StopCatch(sigc) - } - status := <-statusC - code, _, err := status.Result() - if err != nil { - return err - } - if code != 0 { - return ExitCodeError{ - exitCode: int(code), - } - } - return nil -} - -// FIXME: split to smaller functions -func createContainer(ctx context.Context, cmd *cobra.Command, client *containerd.Client, args []string, platform string, flagI, flagT, flagD bool) (containerd.Container, func(), error) { - // simulate the behavior of double dash - newArg := []string{} - if len(args) >= 2 && args[1] == "--" { - newArg = append(newArg, args[:1]...) - newArg = append(newArg, args[2:]...) - args = newArg - } - var internalLabels internalLabels - internalLabels.platform = platform - - ns, err := cmd.Flags().GetString("namespace") - if err != nil { - return nil, nil, err - } - internalLabels.namespace = ns - - var ( - opts []oci.SpecOpts - cOpts []containerd.NewContainerOpts - id = idgen.GenerateID() - ) - - cidfile, err := cmd.Flags().GetString("cidfile") - if err != nil { - return nil, nil, err - } - if cidfile != "" { - if err := writeCIDFile(cidfile, id); err != nil { - return nil, nil, err - } - } - - dataStore, err := getDataStore(cmd) - if err != nil { - return nil, nil, err - } - - stateDir, err := getContainerStateDirPath(cmd, dataStore, id) - if err != nil { - return nil, nil, err - } - if err := os.MkdirAll(stateDir, 0700); err != nil { - return nil, nil, err - } - internalLabels.stateDir = stateDir - - opts = append(opts, - oci.WithDefaultSpec(), - ) - - opts, internalLabels, err = setPlatformOptions(ctx, opts, cmd, client, id, internalLabels) - if err != nil { - return nil, nil, err - } - - rootfsOpts, rootfsCOpts, ensuredImage, err := generateRootfsOpts(ctx, client, platform, cmd, args, id) - if err != nil { - return nil, nil, err - } - opts = append(opts, rootfsOpts...) - cOpts = append(cOpts, rootfsCOpts...) - - wd, err := cmd.Flags().GetString("workdir") - if err != nil { - return nil, nil, err - } - if wd != "" { - opts = append(opts, oci.WithProcessCwd(wd)) - } - - envFile, err := cmd.Flags().GetStringSlice("env-file") - if err != nil { - return nil, nil, err - } - env, err := cmd.Flags().GetStringArray("env") - if err != nil { - return nil, nil, err - } - envs, err := generateEnvs(envFile, env) - if err != nil { - return nil, nil, err - } - opts = append(opts, oci.WithEnv(envs)) - - if flagI { - if flagD { - return nil, nil, errors.New("currently flag -i and -d cannot be specified together (FIXME)") - } - } - - if flagT { - if flagD { - return nil, nil, errors.New("currently flag -t and -d cannot be specified together (FIXME)") - } - opts = append(opts, oci.WithTTY) - } - - mountOpts, anonVolumes, mountPoints, err := generateMountOpts(ctx, cmd, client, ensuredImage) - if err != nil { - return nil, nil, err - } - internalLabels.anonVolumes = anonVolumes - internalLabels.mountPoints = mountPoints - opts = append(opts, mountOpts...) - - var logURI string - if flagD { - // json-file is the built-in and default log driver for nerdctl - logDriver, err := cmd.Flags().GetString("log-driver") - if err != nil { - return nil, nil, err - } - - // check if log driver is a valid uri. If it is a valid uri and scheme is not - if u, err := url.Parse(logDriver); err == nil && u.Scheme != "" { - logURI = logDriver - } else { - logOptMap, err := parseKVStringsMapFromLogOpt(cmd, logDriver) - if err != nil { - return nil, nil, err - } - logDriverInst, err := logging.GetDriver(logDriver, logOptMap) - if err != nil { - return nil, nil, err - } - if err := logDriverInst.Init(dataStore, ns, id); err != nil { - return nil, nil, err - } - logConfig := &logging.LogConfig{ - Driver: logDriver, - Opts: logOptMap, - } - logConfigB, err := json.Marshal(logConfig) - if err != nil { - return nil, nil, err - } - logConfigFilePath := logging.LogConfigFilePath(dataStore, ns, id) - if err = os.WriteFile(logConfigFilePath, logConfigB, 0600); err != nil { - return nil, nil, err - } - if lu, err := generateLogURI(dataStore); err != nil { - return nil, nil, err - } else if lu != nil { - logrus.Debugf("generated log driver: %s", lu.String()) - - logURI = lu.String() - } - } - } - internalLabels.logURI = logURI - - restartValue, err := cmd.Flags().GetString("restart") - if err != nil { - return nil, nil, err - } - restartOpts, err := generateRestartOpts(ctx, client, restartValue, logURI) - if err != nil { - return nil, nil, err - } - cOpts = append(cOpts, restartOpts...) - - stopSignal, err := cmd.Flags().GetString("stop-signal") - if err != nil { - return nil, nil, err - } - stopTimeout, err := cmd.Flags().GetInt("stop-timeout") - if err != nil { - return nil, nil, err - } - cOpts = append(cOpts, withStop(stopSignal, stopTimeout, ensuredImage)) - - hostname := id[0:12] - customHostname, err := cmd.Flags().GetString("hostname") - if err != nil { - return nil, nil, err - } - if customHostname != "" { - hostname = customHostname - } - opts = append(opts, oci.WithHostname(hostname)) - internalLabels.hostname = hostname - // `/etc/hostname` does not exist on FreeBSD - if runtime.GOOS == "linux" { - hostnamePath := filepath.Join(stateDir, "hostname") - if err := os.WriteFile(hostnamePath, []byte(hostname+"\n"), 0644); err != nil { - return nil, nil, err - } - opts = append(opts, withCustomEtcHostname(hostnamePath)) - } - - netOpts, netSlice, ipAddress, ports, macAddress, err := generateNetOpts(cmd, dataStore, stateDir, ns, id) - if err != nil { - return nil, nil, err - } - internalLabels.networks = netSlice - internalLabels.ipAddress = ipAddress - internalLabels.ports = ports - internalLabels.macAddress = macAddress - opts = append(opts, netOpts...) - - hookOpt, err := withNerdctlOCIHook(cmd, id) - if err != nil { - return nil, nil, err - } - opts = append(opts, hookOpt) - - uOpts, err := generateUserOpts(cmd) - if err != nil { - return nil, nil, err - } - opts = append(opts, uOpts...) - - gOpts, err := generateGroupsOpts(cmd) - if err != nil { - return nil, nil, err - } - opts = append(opts, gOpts...) - - umaskOpts, err := generateUmaskOpts(cmd) - if err != nil { - return nil, nil, err - } - opts = append(opts, umaskOpts...) - - rtCOpts, err := generateRuntimeCOpts(cmd) - if err != nil { - return nil, nil, err - } - cOpts = append(cOpts, rtCOpts...) - - lCOpts, err := withContainerLabels(cmd) - if err != nil { - return nil, nil, err - } - cOpts = append(cOpts, lCOpts...) - - var containerNameStore namestore.NameStore - name, err := cmd.Flags().GetString("name") - if err != nil { - return nil, nil, err - } - if name == "" && !cmd.Flags().Changed("name") { - // Automatically set the container name, unless `--name=""` was explicitly specified. - var imageRef string - if ensuredImage != nil { - imageRef = ensuredImage.Ref - } - name = referenceutil.SuggestContainerName(imageRef, id) - } - if name != "" { - containerNameStore, err = namestore.New(dataStore, ns) - if err != nil { - return nil, nil, err - } - if err := containerNameStore.Acquire(name, id); err != nil { - return nil, nil, err - } - } - internalLabels.name = name - - var pidFile string - if cmd.Flags().Lookup("pidfile").Changed { - pidFile, err = cmd.Flags().GetString("pidfile") - if err != nil { - return nil, nil, err - } - } - internalLabels.pidFile = pidFile - - extraHosts, err := cmd.Flags().GetStringSlice("add-host") - if err != nil { - return nil, nil, err - } - extraHosts = strutil.DedupeStrSlice(extraHosts) - for _, host := range extraHosts { - if _, err := dopts.ValidateExtraHost(host); err != nil { - return nil, nil, err - } - } - internalLabels.extraHosts = extraHosts - - ilOpt, err := withInternalLabels(internalLabels) - if err != nil { - return nil, nil, err - } - cOpts = append(cOpts, ilOpt) - - opts = append(opts, propagateContainerdLabelsToOCIAnnotations()) - - var s specs.Spec - spec := containerd.WithSpec(&s, opts...) - cOpts = append(cOpts, spec) - - container, err := client.NewContainer(ctx, id, cOpts...) - if err != nil { - gcContainer := func() { - var isErr bool - if errE := os.RemoveAll(stateDir); errE != nil { - isErr = true - } - if name != "" { - var errE error - if containerNameStore, errE = namestore.New(dataStore, ns); errE != nil { - isErr = true - } - if errE = containerNameStore.Release(name, id); errE != nil { - isErr = true - } - - } - if isErr { - logrus.Warnf("failed to remove container %q", id) - } - } - return nil, gcContainer, err - } - return container, nil, nil -} - -func generateRootfsOpts(ctx context.Context, client *containerd.Client, platform string, cmd *cobra.Command, args []string, id string) ([]oci.SpecOpts, []containerd.NewContainerOpts, *imgutil.EnsuredImage, error) { - var ( - ensured *imgutil.EnsuredImage - err error - ) - imageless, err := cmd.Flags().GetBool("rootfs") - if err != nil { - return nil, nil, nil, err - } - if !imageless { - pull, err := cmd.Flags().GetString("pull") - if err != nil { - return nil, nil, nil, err - } - var platformSS []string // len: 0 or 1 - if platform != "" { - platformSS = append(platformSS, platform) - } - ocispecPlatforms, err := platformutil.NewOCISpecPlatformSlice(false, platformSS) - if err != nil { - return nil, nil, nil, err - } - rawRef := args[0] - ensured, err = ensureImage(ctx, cmd, client, rawRef, ocispecPlatforms, pull, nil, false) - if err != nil { - return nil, nil, nil, err - } - } - var ( - opts []oci.SpecOpts - cOpts []containerd.NewContainerOpts - ) - if !imageless { - cOpts = append(cOpts, - containerd.WithImage(ensured.Image), - containerd.WithSnapshotter(ensured.Snapshotter), - containerd.WithNewSnapshot(id, ensured.Image), - containerd.WithImageStopSignal(ensured.Image, "SIGTERM"), - ) - - if len(ensured.ImageConfig.Env) == 0 { - opts = append(opts, oci.WithDefaultPathEnv) - } - for ind, env := range ensured.ImageConfig.Env { - if strings.HasPrefix(env, "PATH=") { - break - } else { - if ind == len(ensured.ImageConfig.Env)-1 { - opts = append(opts, oci.WithDefaultPathEnv) - } - } - } - } else { - absRootfs, err := filepath.Abs(args[0]) - if err != nil { - return nil, nil, nil, err - } - opts = append(opts, oci.WithRootFSPath(absRootfs), oci.WithDefaultPathEnv) - } - - // NOTE: "--entrypoint" can be set to an empty string, see TestRunEntrypoint* in run_test.go . - entrypoint, err := cmd.Flags().GetStringArray("entrypoint") - if err != nil { - return nil, nil, nil, err - } - - if !imageless && !cmd.Flag("entrypoint").Changed { - opts = append(opts, oci.WithImageConfigArgs(ensured.Image, args[1:])) - } else { - if !imageless { - opts = append(opts, oci.WithImageConfig(ensured.Image)) - } - var processArgs []string - if len(entrypoint) != 0 { - processArgs = append(processArgs, entrypoint...) - } - if len(args) > 1 { - processArgs = append(processArgs, args[1:]...) - } - if len(processArgs) == 0 { - // error message is from Podman - return nil, nil, nil, errors.New("no command or entrypoint provided, and no CMD or ENTRYPOINT from image") - } - opts = append(opts, oci.WithProcessArgs(processArgs...)) - } - - initProcessFlag, err := cmd.Flags().GetBool("init") - if err != nil { - return nil, nil, nil, err - } - initBinary, err := cmd.Flags().GetString("init-binary") - if err != nil { - return nil, nil, nil, err - } - if cmd.Flags().Changed("init-binary") { - initProcessFlag = true - } - if initProcessFlag { - binaryPath, err := exec.LookPath(initBinary) - if err != nil { - if errors.Is(err, exec.ErrNotFound) { - return nil, nil, nil, fmt.Errorf(`init binary %q not found`, initBinary) - } - return nil, nil, nil, err - } - inContainerPath := filepath.Join("/sbin", filepath.Base(initBinary)) - opts = append(opts, func(_ context.Context, _ oci.Client, _ *containers.Container, spec *oci.Spec) error { - spec.Process.Args = append([]string{inContainerPath, "--"}, spec.Process.Args...) - spec.Mounts = append([]specs.Mount{{ - Destination: inContainerPath, - Type: "bind", - Source: binaryPath, - Options: []string{"bind", "ro"}, - }}, spec.Mounts...) - return nil - }) - } - - readonly, err := cmd.Flags().GetBool("read-only") - if err != nil { - return nil, nil, nil, err - } - if readonly { - opts = append(opts, oci.WithRootFSReadonly()) - } - return opts, cOpts, ensured, nil -} - -// withBindMountHostIPC replaces /dev/shm and /dev/mqueue mount with rbind. -// Required for --ipc=host on rootless. -func withBindMountHostIPC(_ context.Context, _ oci.Client, _ *containers.Container, s *oci.Spec) error { - for i, m := range s.Mounts { - if path.Clean(m.Destination) == "/dev/shm" { - newM := specs.Mount{ - Destination: "/dev/shm", - Type: "bind", - Source: "/dev/shm", - Options: []string{"rbind", "nosuid", "noexec", "nodev"}, - } - s.Mounts[i] = newM - } - if path.Clean(m.Destination) == "/dev/mqueue" { - newM := specs.Mount{ - Destination: "/dev/mqueue", - Type: "bind", - Source: "/dev/mqueue", - Options: []string{"rbind", "nosuid", "noexec", "nodev"}, - } - s.Mounts[i] = newM - } - } - return nil -} - -// withBindMountHostProcfs replaces procfs mount with rbind. -// Required for --pid=host on rootless. -// -// https://github.com/moby/moby/pull/41893/files -// https://github.com/containers/podman/blob/v3.0.0-rc1/pkg/specgen/generate/oci.go#L248-L257 -func withBindMountHostProcfs(_ context.Context, _ oci.Client, _ *containers.Container, s *oci.Spec) error { - for i, m := range s.Mounts { - if path.Clean(m.Destination) == "/proc" { - newM := specs.Mount{ - Destination: "/proc", - Type: "bind", - Source: "/proc", - Options: []string{"rbind", "nosuid", "noexec", "nodev"}, - } - s.Mounts[i] = newM - } - } - - // Remove ReadonlyPaths for /proc/* - newROP := s.Linux.ReadonlyPaths[:0] - for _, x := range s.Linux.ReadonlyPaths { - x = path.Clean(x) - if !strings.HasPrefix(x, "/proc/") { - newROP = append(newROP, x) - } - } - s.Linux.ReadonlyPaths = newROP - return nil -} - -func generateLogURI(dataStore string) (*url.URL, error) { - selfExe, err := os.Executable() - if err != nil { - return nil, err - } - args := map[string]string{ - logging.MagicArgv1: dataStore, - } - - return cio.LogURIGenerator("binary", selfExe, args) -} - -func withNerdctlOCIHook(cmd *cobra.Command, id string) (oci.SpecOpts, error) { - selfExe, f := globalFlags(cmd) - args := append([]string{selfExe}, append(f, "internal", "oci-hook")...) - return func(_ context.Context, _ oci.Client, _ *containers.Container, s *specs.Spec) error { - if s.Hooks == nil { - s.Hooks = &specs.Hooks{} - } - crArgs := append(args, "createRuntime") - s.Hooks.CreateRuntime = append(s.Hooks.CreateRuntime, specs.Hook{ - Path: selfExe, - Args: crArgs, - Env: os.Environ(), - }) - argsCopy := append([]string(nil), args...) - psArgs := append(argsCopy, "postStop") - s.Hooks.Poststop = append(s.Hooks.Poststop, specs.Hook{ - Path: selfExe, - Args: psArgs, - Env: os.Environ(), - }) - return nil - }, nil -} - -func getContainerStateDirPath(cmd *cobra.Command, dataStore, id string) (string, error) { - ns, err := cmd.Flags().GetString("namespace") - if err != nil { - return "", err - } - if ns == "" { - return "", errors.New("namespace is required") - } - if strings.Contains(ns, "/") { - return "", errors.New("namespace with '/' is unsupported") - } - return filepath.Join(dataStore, "containers", ns, id), nil -} - -func withContainerLabels(cmd *cobra.Command) ([]containerd.NewContainerOpts, error) { - labelMap, err := readKVStringsMapfFromLabel(cmd) - if err != nil { - return nil, err - } - o := containerd.WithAdditionalContainerLabels(labelMap) - return []containerd.NewContainerOpts{o}, nil -} - -func readKVStringsMapfFromLabel(cmd *cobra.Command) (map[string]string, error) { - labelsMap, err := cmd.Flags().GetStringArray("label") - if err != nil { - return nil, err - } - labelsMap = strutil.DedupeStrSlice(labelsMap) - labelsFilePath, err := cmd.Flags().GetStringSlice("label-file") - if err != nil { - return nil, err - } - labelsFilePath = strutil.DedupeStrSlice(labelsFilePath) - labels, err := dopts.ReadKVStrings(labelsFilePath, labelsMap) - if err != nil { - return nil, err - } - - return strutil.ConvertKVStringsToMap(labels), nil -} - -// parseKVStringsMapFromLogOpt parse log options KV entries and convert to Map -func parseKVStringsMapFromLogOpt(cmd *cobra.Command, logDriver string) (map[string]string, error) { - logOptArray, err := cmd.Flags().GetStringArray("log-opt") - if err != nil { - return nil, err - } - logOptArray = strutil.DedupeStrSlice(logOptArray) - logOptMap := strutil.ConvertKVStringsToMap(logOptArray) - if logDriver == "json-file" { - if _, ok := logOptMap[logging.MaxSize]; !ok { - delete(logOptMap, logging.MaxFile) - } - } - if err := logging.ValidateLogOpts(logDriver, logOptMap); err != nil { - return nil, err - } - return logOptMap, nil -} - -func withStop(stopSignal string, stopTimeout int, ensuredImage *imgutil.EnsuredImage) containerd.NewContainerOpts { - return func(ctx context.Context, _ *containerd.Client, c *containers.Container) error { - if c.Labels == nil { - c.Labels = make(map[string]string) - } - var err error - if ensuredImage != nil { - stopSignal, err = containerd.GetOCIStopSignal(ctx, ensuredImage.Image, stopSignal) - if err != nil { - return err - } - } - c.Labels[containerd.StopSignalLabel] = stopSignal - if stopTimeout != 0 { - c.Labels[labels.StopTimout] = strconv.Itoa(stopTimeout) - } - return nil - } -} - -type internalLabels struct { - // labels from cmd options - namespace string - platform string - extraHosts []string - pidFile string - // labels from cmd options or automatically set - name string - hostname string - // automatically generated - stateDir string - // network - networks []string - ipAddress string - ports []gocni.PortMapping - macAddress string - // volumn - mountPoints []*mountutil.Processed - anonVolumes []string - // pid namespace - pidContainer string - // log - logURI string -} - -func withInternalLabels(internalLabels internalLabels) (containerd.NewContainerOpts, error) { - m := make(map[string]string) - m[labels.Namespace] = internalLabels.namespace - if internalLabels.name != "" { - m[labels.Name] = internalLabels.name - } - m[labels.Hostname] = internalLabels.hostname - extraHostsJSON, err := json.Marshal(internalLabels.extraHosts) - if err != nil { - return nil, err - } - m[labels.ExtraHosts] = string(extraHostsJSON) - m[labels.StateDir] = internalLabels.stateDir - networksJSON, err := json.Marshal(internalLabels.networks) - if err != nil { - return nil, err - } - m[labels.Networks] = string(networksJSON) - if len(internalLabels.ports) > 0 { - portsJSON, err := json.Marshal(internalLabels.ports) - if err != nil { - return nil, err - } - m[labels.Ports] = string(portsJSON) - } - if internalLabels.logURI != "" { - m[labels.LogURI] = internalLabels.logURI - } - if len(internalLabels.anonVolumes) > 0 { - anonVolumeJSON, err := json.Marshal(internalLabels.anonVolumes) - if err != nil { - return nil, err - } - m[labels.AnonymousVolumes] = string(anonVolumeJSON) - } - - if internalLabels.pidFile != "" { - m[labels.PIDFile] = internalLabels.pidFile - } - - if internalLabels.ipAddress != "" { - m[labels.IPAddress] = internalLabels.ipAddress - } - - m[labels.Platform], err = platformutil.NormalizeString(internalLabels.platform) - if err != nil { - return nil, err - } - - if len(internalLabels.mountPoints) > 0 { - mounts := dockercompatMounts(internalLabels.mountPoints) - mountPointsJSON, err := json.Marshal(mounts) - if err != nil { - return nil, err - } - m[labels.Mounts] = string(mountPointsJSON) - } - - if internalLabels.macAddress != "" { - m[labels.MACAddress] = internalLabels.macAddress - } - - if internalLabels.pidContainer != "" { - m[labels.PIDContainer] = internalLabels.pidContainer - } - - return containerd.WithAdditionalContainerLabels(m), nil -} - -func dockercompatMounts(mountPoints []*mountutil.Processed) []dockercompat.MountPoint { - reuslt := make([]dockercompat.MountPoint, len(mountPoints)) - for i := range mountPoints { - mp := mountPoints[i] - reuslt[i] = dockercompat.MountPoint{ - Type: mp.Type, - Name: mp.Name, - Source: mp.Mount.Source, - Destination: mp.Mount.Destination, - Driver: "", - Mode: mp.Mode, - } - - // it's a anonymous volume - if mp.AnonymousVolume != "" { - reuslt[i].Name = mp.AnonymousVolume - } - - // volume only support local driver - if mp.Type == "volume" { - reuslt[i].Driver = "local" - } - } - return reuslt -} - -func propagateContainerdLabelsToOCIAnnotations() oci.SpecOpts { - return func(ctx context.Context, oc oci.Client, c *containers.Container, s *oci.Spec) error { - return oci.WithAnnotations(c.Labels)(ctx, oc, c, s) - } -} - -func writeCIDFile(path, id string) error { - if _, err := os.Stat(path); err == nil { - return fmt.Errorf("container ID file found, make sure the other container isn't running or delete %s", path) - } else if errors.Is(err, os.ErrNotExist) { - f, err := os.Create(path) - if err != nil { - return err - } - defer f.Close() - if err != nil { - return fmt.Errorf("failed to create the container ID file: %s", err) - } - if _, err := f.WriteString(id); err != nil { - return err - } - return nil - } else { - return err - } -} - -func parseEnvVars(paths []string) ([]string, error) { - vars := make([]string, 0) - for _, path := range paths { - f, err := os.Open(path) - if err != nil { - return nil, fmt.Errorf("failed to open env file %s: %w", path, err) - } - defer f.Close() - - sc := bufio.NewScanner(f) - for sc.Scan() { - line := strings.TrimSpace(sc.Text()) - // skip comment lines - if strings.HasPrefix(line, "#") { - continue - } - vars = append(vars, line) - } - if err = sc.Err(); err != nil { - return nil, err - } - } - return vars, nil -} - -func withOSEnv(envs []string) ([]string, error) { - newEnvs := make([]string, len(envs)) - - // from https://github.com/docker/cli/blob/v22.06.0-beta.0/opts/env.go#L18 - getEnv := func(val string) (string, error) { - arr := strings.SplitN(val, "=", 2) - if arr[0] == "" { - return "", errors.New("invalid environment variable: " + val) - } - if len(arr) > 1 { - return val, nil - } - if envVal, ok := os.LookupEnv(arr[0]); ok { - return arr[0] + "=" + envVal, nil - } - return val, nil - } - for i := range envs { - env, err := getEnv(envs[i]) - if err != nil { - return nil, err - } - newEnvs[i] = env - } - - return newEnvs, nil -} - -func generateSharingPIDOpts(ctx context.Context, targetCon containerd.Container) ([]oci.SpecOpts, error) { - opts := make([]oci.SpecOpts, 0) - - task, err := targetCon.Task(ctx, nil) - if err != nil { - return nil, err - } - status, err := task.Status(ctx) - if err != nil { - return nil, err - } - - if status.Status != containerd.Running { - return nil, fmt.Errorf("shared container is not running") - } - - spec, err := targetCon.Spec(ctx) - if err != nil { - return nil, err - } - - isHost := true - for _, n := range spec.Linux.Namespaces { - if n.Type == specs.PIDNamespace { - isHost = false - } - } - if isHost { - opts = append(opts, oci.WithHostNamespace(specs.PIDNamespace)) - if rootlessutil.IsRootless() { - opts = append(opts, withBindMountHostProcfs) - } - } else { - ns := specs.LinuxNamespace{ - Type: specs.PIDNamespace, - Path: fmt.Sprintf("/proc/%d/ns/pid", task.Pid()), - } - opts = append(opts, oci.WithLinuxNamespace(ns)) - } - - return opts, nil -} - -// generateEnvs combines environment variables from `--env-file` and `--env`. -// Pass an empty slice if any arg is not used. -func generateEnvs(envFile []string, env []string) ([]string, error) { - var envs []string - var err error - - if envFiles := strutil.DedupeStrSlice(envFile); len(envFiles) > 0 { - envs, err = parseEnvVars(envFiles) - if err != nil { - return nil, err - } - } - - if env := strutil.DedupeStrSlice(env); len(env) > 0 { - envs = append(envs, env...) - } - - if envs, err = withOSEnv(envs); err != nil { - return nil, err - } - - return envs, nil -} diff --git a/cmd/nerdctl/events.go b/cmd/nerdctl/system/events.go similarity index 92% rename from cmd/nerdctl/events.go rename to cmd/nerdctl/system/events.go index 916dcb4e82d..6428f5f9441 100644 --- a/cmd/nerdctl/events.go +++ b/cmd/nerdctl/system/events.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package system import ( "bytes" @@ -26,6 +26,8 @@ import ( "github.com/containerd/containerd/events" "github.com/containerd/containerd/log" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/fmtutil" "github.com/containerd/typeurl" "github.com/spf13/cobra" @@ -34,7 +36,7 @@ import ( _ "github.com/containerd/containerd/api/events" ) -func newEventsCommand() *cobra.Command { +func NewEventsCommand() *cobra.Command { shortHelp := `Get real time events from the server` longHelp := shortHelp + "\nNOTE: The output format is not compatible with Docker." var eventsCommand = &cobra.Command{ @@ -62,7 +64,7 @@ type Out struct { // eventsActions is from https://github.com/containerd/containerd/blob/v1.4.3/cmd/ctr/commands/events/events.go func eventsAction(cmd *cobra.Command, args []string) error { - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } @@ -81,7 +83,7 @@ func eventsAction(cmd *cobra.Command, args []string) error { case "raw", "table", "wide": return errors.New("unsupported format: \"raw\", \"table\", and \"wide\"") default: - tmpl, err = parseTemplate(format) + tmpl, err = fmtutil.ParseTemplate(format) if err != nil { return err } diff --git a/cmd/nerdctl/info.go b/cmd/nerdctl/system/info.go similarity index 96% rename from cmd/nerdctl/info.go rename to cmd/nerdctl/system/info.go index 9268492e500..5760e13e213 100644 --- a/cmd/nerdctl/info.go +++ b/cmd/nerdctl/system/info.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package system import ( "fmt" @@ -23,6 +23,8 @@ import ( "strings" "text/template" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/fmtutil" "golang.org/x/text/cases" "golang.org/x/text/language" @@ -37,7 +39,7 @@ import ( "github.com/spf13/cobra" ) -func newInfoCommand() *cobra.Command { +func NewInfoCommand() *cobra.Command { var infoCommand = &cobra.Command{ Use: "info", Args: cobra.NoArgs, @@ -67,13 +69,13 @@ func infoAction(cmd *cobra.Command, args []string) error { return err } if format != "" { - tmpl, err = parseTemplate(format) + tmpl, err = fmtutil.ParseTemplate(format) if err != nil { return err } } - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } diff --git a/cmd/nerdctl/system.go b/cmd/nerdctl/system/system.go similarity index 67% rename from cmd/nerdctl/system.go rename to cmd/nerdctl/system/system.go index bb9635be295..9565d4df47d 100644 --- a/cmd/nerdctl/system.go +++ b/cmd/nerdctl/system/system.go @@ -14,24 +14,28 @@ limitations under the License. */ -package main +package system -import "github.com/spf13/cobra" +import ( + "github.com/containerd/nerdctl/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/common" + "github.com/spf13/cobra" +) -func newSystemCommand() *cobra.Command { +func NewSystemCommand() *cobra.Command { var systemCommand = &cobra.Command{ - Annotations: map[string]string{Category: Management}, + Annotations: map[string]string{common.Category: common.Management}, Use: "system", Short: "Manage containerd", - RunE: unknownSubcommandAction, + RunE: completion.UnknownSubcommandAction, SilenceUsage: true, SilenceErrors: true, } // versionCommand is not here systemCommand.AddCommand( - newEventsCommand(), - newInfoCommand(), - newSystemPruneCommand(), + NewEventsCommand(), + NewInfoCommand(), + NewPruneCommand(), ) return systemCommand } diff --git a/cmd/nerdctl/system_prune.go b/cmd/nerdctl/system/system_prune.go similarity index 79% rename from cmd/nerdctl/system_prune.go rename to cmd/nerdctl/system/system_prune.go index 544c6a271f1..f493e21ff4f 100644 --- a/cmd/nerdctl/system_prune.go +++ b/cmd/nerdctl/system/system_prune.go @@ -14,17 +14,22 @@ limitations under the License. */ -package main +package system import ( "fmt" "strings" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/container" + "github.com/containerd/nerdctl/cmd/nerdctl/image" + "github.com/containerd/nerdctl/cmd/nerdctl/network" + "github.com/containerd/nerdctl/cmd/nerdctl/volume" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) -func newSystemPruneCommand() *cobra.Command { +func NewPruneCommand() *cobra.Command { systemPruneCommand := &cobra.Command{ Use: "prune [flags]", Short: "Remove unused data", @@ -82,22 +87,22 @@ func systemPruneAction(cmd *cobra.Command, args []string) error { } } - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } defer cancel() - if err := containerPrune(ctx, cmd, client); err != nil { + if err := container.Prune(ctx, cmd, client); err != nil { return err } - if err := networkPrune(ctx, cmd, client); err != nil { + if err := network.Prune(ctx, cmd, client); err != nil { return err } if vFlag { - if err := volumePrune(ctx, cmd, client); err != nil { + if err := volume.Prune(ctx, cmd, client); err != nil { return err } } - return imagePrune(ctx, cmd, client) + return image.Prune(ctx, cmd, client) } diff --git a/cmd/nerdctl/utils/action/pause.go b/cmd/nerdctl/utils/action/pause.go new file mode 100644 index 00000000000..c6188a33f1f --- /dev/null +++ b/cmd/nerdctl/utils/action/pause.go @@ -0,0 +1,51 @@ +/* + 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 action + +import ( + "context" + "fmt" + + "github.com/containerd/containerd" + "github.com/containerd/containerd/cio" +) + +func PauseContainer(ctx context.Context, client *containerd.Client, id string) error { + container, err := client.LoadContainer(ctx, id) + if err != nil { + return err + } + + task, err := container.Task(ctx, cio.Load) + if err != nil { + return err + } + + status, err := task.Status(ctx) + if err != nil { + return err + } + + switch status.Status { + case containerd.Paused: + return fmt.Errorf("container %s is already paused", id) + case containerd.Created, containerd.Stopped: + return fmt.Errorf("container %s is not running", id) + default: + return task.Pause(ctx) + } +} diff --git a/cmd/nerdctl/rm.go b/cmd/nerdctl/utils/action/remove.go similarity index 60% rename from cmd/nerdctl/rm.go rename to cmd/nerdctl/utils/action/remove.go index 46d588a4678..0a6196bf064 100644 --- a/cmd/nerdctl/rm.go +++ b/cmd/nerdctl/utils/action/remove.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package action import ( "context" @@ -27,81 +27,17 @@ import ( "github.com/containerd/containerd/cio" "github.com/containerd/containerd/errdefs" "github.com/containerd/containerd/namespaces" + "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/common" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/volume" "github.com/containerd/nerdctl/pkg/dnsutil/hostsstore" - "github.com/containerd/nerdctl/pkg/idutil/containerwalker" "github.com/containerd/nerdctl/pkg/labels" "github.com/containerd/nerdctl/pkg/namestore" - "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) -func newRmCommand() *cobra.Command { - var rmCommand = &cobra.Command{ - Use: "rm [flags] CONTAINER [CONTAINER, ...]", - Args: cobra.MinimumNArgs(1), - Short: "Remove one or more containers", - RunE: rmAction, - ValidArgsFunction: rmShellComplete, - SilenceUsage: true, - SilenceErrors: true, - } - rmCommand.Flags().BoolP("force", "f", false, "Force the removal of a running|paused|unknown container (uses SIGKILL)") - rmCommand.Flags().BoolP("volumes", "v", false, "Remove volumes associated with the container") - return rmCommand -} - -// removing a non-stoped/non-created container without force, will cause a error -type statusError struct { - error -} - -func rmAction(cmd *cobra.Command, args []string) error { - force, err := cmd.Flags().GetBool("force") - if err != nil { - return err - } - removeAnonVolumes, err := cmd.Flags().GetBool("volumes") - if err != nil { - return err - } - - client, ctx, cancel, err := newClient(cmd) - if err != nil { - return err - } - defer cancel() - - walker := &containerwalker.ContainerWalker{ - Client: client, - OnFound: func(ctx context.Context, found containerwalker.Found) error { - if found.MatchCount > 1 { - return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req) - } - if err := removeContainer(ctx, cmd, found.Container, force, removeAnonVolumes); err != nil { - return err - } - _, err = fmt.Fprintf(cmd.OutOrStdout(), "%s\n", found.Req) - return err - }, - } - for _, req := range args { - n, err := walker.Walk(ctx, req) - if err == nil && n == 0 { - err = fmt.Errorf("no such container %s", req) - } - if err != nil { - if force { - logrus.Error(err) - } else { - return err - } - } - } - return nil -} - -func removeContainer(ctx context.Context, cmd *cobra.Command, container containerd.Container, force bool, removeAnonVolumes bool) (retErr error) { +func RemoveContainer(ctx context.Context, cmd *cobra.Command, container containerd.Container, force bool, removeAnonVolumes bool) (retErr error) { ns, err := namespaces.NamespaceRequired(ctx) if err != nil { return err @@ -115,7 +51,7 @@ func removeContainer(ctx context.Context, cmd *cobra.Command, container containe stateDir := l[labels.StateDir] name := l[labels.Name] - dataStore, err := getDataStore(cmd) + dataStore, err := client.DataStore(cmd) if err != nil { return err } @@ -137,7 +73,7 @@ func removeContainer(ctx context.Context, cmd *cobra.Command, container containe } if name != "" { if err := namst.Release(name, id); err != nil { - logrus.WithError(retErr).Warnf("failed to release container name %s", name) + logrus.WithError(retErr).Warnf("failed to release container Name %s", name) } } if err := hostsstore.DeallocHostsFile(dataStore, ns, id); err != nil { @@ -149,13 +85,13 @@ func removeContainer(ctx context.Context, cmd *cobra.Command, container containe if err := json.Unmarshal([]byte(anonVolumesJSON), &anonVolumes); err != nil { return err } - volStore, err := getVolumeStore(cmd) + volStore, err := volume.Store(cmd) if err != nil { return err } defer func() { if _, err := volStore.Remove(anonVolumes); err != nil { - logrus.WithError(err).Warnf("failed to remove anonymous volumes %v", anonVolumes) + logrus.WithError(err).Warnf("failed to remove anonymous volume %v", anonVolumes) } }() } @@ -185,7 +121,7 @@ func removeContainer(ctx context.Context, cmd *cobra.Command, container containe } case containerd.Paused: if !force { - return statusError{fmt.Errorf("you cannot remove a %v container %v. Unpause the container before attempting removal or force remove", status.Status, id)} + return common.NewStatusError(fmt.Errorf("you cannot remove a %v container %v. Unpause the container before attempting removal or force remove", status.Status, id)) } _, err := task.Delete(ctx, containerd.WithProcessKill) if err != nil && !errdefs.IsNotFound(err) { @@ -194,7 +130,7 @@ func removeContainer(ctx context.Context, cmd *cobra.Command, container containe // default is the case, when status.Status = containerd.Running default: if !force { - return statusError{fmt.Errorf("you cannot remove a %v container %v. Stop the container before attempting removal or force remove", status.Status, id)} + return common.NewStatusError(fmt.Errorf("you cannot remove a %v container %v. Stop the container before attempting removal or force remove", status.Status, id)) } if err := task.Kill(ctx, syscall.SIGKILL); err != nil { logrus.WithError(err).Warnf("failed to send SIGKILL") @@ -218,8 +154,3 @@ func removeContainer(ctx context.Context, cmd *cobra.Command, container containe } return err } - -func rmShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - // show container names - return shellCompleteContainerNames(cmd, nil) -} diff --git a/cmd/nerdctl/utils/action/run/create.go b/cmd/nerdctl/utils/action/run/create.go new file mode 100644 index 00000000000..d59b1b7cd81 --- /dev/null +++ b/cmd/nerdctl/utils/action/run/create.go @@ -0,0 +1,689 @@ +/* + 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 run + +import ( + "bufio" + "context" + "encoding/json" + "errors" + "fmt" + "net/url" + "os" + "os/exec" + "path/filepath" + "runtime" + "strconv" + "strings" + + "github.com/containerd/containerd" + "github.com/containerd/containerd/cio" + "github.com/containerd/containerd/containers" + "github.com/containerd/containerd/oci" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/utils" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/common" + "github.com/containerd/nerdctl/pkg/idgen" + "github.com/containerd/nerdctl/pkg/imgutil" + "github.com/containerd/nerdctl/pkg/labels" + "github.com/containerd/nerdctl/pkg/logging" + "github.com/containerd/nerdctl/pkg/namestore" + "github.com/containerd/nerdctl/pkg/platformutil" + "github.com/containerd/nerdctl/pkg/referenceutil" + "github.com/containerd/nerdctl/pkg/strutil" + opts2 "github.com/docker/cli/opts" + "github.com/opencontainers/runtime-spec/specs-go" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +// FIXME: split to smaller functions +func CreateContainer(ctx context.Context, cmd *cobra.Command, client *containerd.Client, args []string, platform string, flagI, flagT, flagD bool) (containerd.Container, func(), error) { + // simulate the behavior of double dash + newArg := []string{} + if len(args) >= 2 && args[1] == "--" { + newArg = append(newArg, args[:1]...) + newArg = append(newArg, args[2:]...) + args = newArg + } + var internalLabels common.InternalLabels + internalLabels.Platform = platform + + ns, err := cmd.Flags().GetString("namespace") + if err != nil { + return nil, nil, err + } + internalLabels.Namespace = ns + + var ( + opts []oci.SpecOpts + cOpts []containerd.NewContainerOpts + id = idgen.GenerateID() + ) + + cidfile, err := cmd.Flags().GetString("cidfile") + if err != nil { + return nil, nil, err + } + if cidfile != "" { + if err := WriteCIDFile(cidfile, id); err != nil { + return nil, nil, err + } + } + + dataStore, err := ncclient.DataStore(cmd) + if err != nil { + return nil, nil, err + } + + stateDir, err := common.ContainerStateDirPath(cmd, dataStore, id) + if err != nil { + return nil, nil, err + } + if err := os.MkdirAll(stateDir, 0700); err != nil { + return nil, nil, err + } + internalLabels.StateDir = stateDir + + opts = append(opts, + oci.WithDefaultSpec(), + ) + + opts, internalLabels, err = SetPlatformOptions(ctx, opts, cmd, client, id, internalLabels) + if err != nil { + return nil, nil, err + } + + rootfsOpts, rootfsCOpts, ensuredImage, err := GenerateRootfsOpts(ctx, client, platform, cmd, args, id) + if err != nil { + return nil, nil, err + } + opts = append(opts, rootfsOpts...) + cOpts = append(cOpts, rootfsCOpts...) + + wd, err := cmd.Flags().GetString("workdir") + if err != nil { + return nil, nil, err + } + if wd != "" { + opts = append(opts, oci.WithProcessCwd(wd)) + } + + envFile, err := cmd.Flags().GetStringSlice("env-file") + if err != nil { + return nil, nil, err + } + env, err := cmd.Flags().GetStringArray("env") + if err != nil { + return nil, nil, err + } + envs, err := GenerateEnvs(envFile, env) + if err != nil { + return nil, nil, err + } + opts = append(opts, oci.WithEnv(envs)) + + if flagI { + if flagD { + return nil, nil, errors.New("currently flag -i and -d cannot be specified together (FIXME)") + } + } + + if flagT { + if flagD { + return nil, nil, errors.New("currently flag -t and -d cannot be specified together (FIXME)") + } + opts = append(opts, oci.WithTTY) + } + + mountOpts, anonVolumes, mountPoints, err := GenerateMountOpts(ctx, cmd, client, ensuredImage) + if err != nil { + return nil, nil, err + } + internalLabels.AnonVolumes = anonVolumes + internalLabels.MountPoints = mountPoints + opts = append(opts, mountOpts...) + + var logURI string + if flagD { + // json-file is the built-in and default log driver for nerdctl + logDriver, err := cmd.Flags().GetString("log-driver") + if err != nil { + return nil, nil, err + } + + // check if log driver is a valid uri. If it is a valid uri and scheme is not + if u, err := url.Parse(logDriver); err == nil && u.Scheme != "" { + logURI = logDriver + } else { + logOptMap, err := ParseKVStringsMapFromLogOpt(cmd, logDriver) + if err != nil { + return nil, nil, err + } + logDriverInst, err := logging.GetDriver(logDriver, logOptMap) + if err != nil { + return nil, nil, err + } + if err := logDriverInst.Init(dataStore, ns, id); err != nil { + return nil, nil, err + } + logConfig := &logging.LogConfig{ + Driver: logDriver, + Opts: logOptMap, + } + logConfigB, err := json.Marshal(logConfig) + if err != nil { + return nil, nil, err + } + logConfigFilePath := logging.LogConfigFilePath(dataStore, ns, id) + if err = os.WriteFile(logConfigFilePath, logConfigB, 0600); err != nil { + return nil, nil, err + } + if lu, err := GenerateLogURI(dataStore); err != nil { + return nil, nil, err + } else if lu != nil { + logrus.Debugf("generated log driver: %s", lu.String()) + + logURI = lu.String() + } + } + } + internalLabels.LogURI = logURI + + restartValue, err := cmd.Flags().GetString("restart") + if err != nil { + return nil, nil, err + } + restartOpts, err := GenerateRestartOpts(ctx, client, restartValue, logURI) + if err != nil { + return nil, nil, err + } + cOpts = append(cOpts, restartOpts...) + + stopSignal, err := cmd.Flags().GetString("stop-signal") + if err != nil { + return nil, nil, err + } + stopTimeout, err := cmd.Flags().GetInt("stop-timeout") + if err != nil { + return nil, nil, err + } + cOpts = append(cOpts, WithStop(stopSignal, stopTimeout, ensuredImage)) + + hostname := id[0:12] + customHostname, err := cmd.Flags().GetString("hostname") + if err != nil { + return nil, nil, err + } + if customHostname != "" { + hostname = customHostname + } + opts = append(opts, oci.WithHostname(hostname)) + internalLabels.Hostname = hostname + // `/etc/hostname` does not exist on FreeBSD + if runtime.GOOS == "linux" { + hostnamePath := filepath.Join(stateDir, "hostname") + if err := os.WriteFile(hostnamePath, []byte(hostname+"\n"), 0644); err != nil { + return nil, nil, err + } + opts = append(opts, WithCustomEtcHostname(hostnamePath)) + } + + netOpts, netSlice, ipAddress, ports, macAddress, err := GenerateNetOpts(cmd, dataStore, stateDir, ns, id) + if err != nil { + return nil, nil, err + } + internalLabels.Networks = netSlice + internalLabels.IPAddress = ipAddress + internalLabels.Ports = ports + internalLabels.MacAddress = macAddress + opts = append(opts, netOpts...) + + hookOpt, err := WithNerdctlOCIHook(cmd, id) + if err != nil { + return nil, nil, err + } + opts = append(opts, hookOpt) + + uOpts, err := GenerateUserOpts(cmd) + if err != nil { + return nil, nil, err + } + opts = append(opts, uOpts...) + + gOpts, err := GenerateGroupsOpts(cmd) + if err != nil { + return nil, nil, err + } + opts = append(opts, gOpts...) + + umaskOpts, err := GenerateUmaskOpts(cmd) + if err != nil { + return nil, nil, err + } + opts = append(opts, umaskOpts...) + + rtCOpts, err := GenerateRuntimeCOpts(cmd) + if err != nil { + return nil, nil, err + } + cOpts = append(cOpts, rtCOpts...) + + lCOpts, err := WithContainerLabels(cmd) + if err != nil { + return nil, nil, err + } + cOpts = append(cOpts, lCOpts...) + + var containerNameStore namestore.NameStore + name, err := cmd.Flags().GetString("name") + if err != nil { + return nil, nil, err + } + if name == "" && !cmd.Flags().Changed("name") { + // Automatically set the container Name, unless `--Name=""` was explicitly specified. + var imageRef string + if ensuredImage != nil { + imageRef = ensuredImage.Ref + } + name = referenceutil.SuggestContainerName(imageRef, id) + } + if name != "" { + containerNameStore, err = namestore.New(dataStore, ns) + if err != nil { + return nil, nil, err + } + if err := containerNameStore.Acquire(name, id); err != nil { + return nil, nil, err + } + } + internalLabels.Name = name + + var pidFile string + if cmd.Flags().Lookup("pidfile").Changed { + pidFile, err = cmd.Flags().GetString("pidfile") + if err != nil { + return nil, nil, err + } + } + internalLabels.PidFile = pidFile + + extraHosts, err := cmd.Flags().GetStringSlice("add-host") + if err != nil { + return nil, nil, err + } + extraHosts = strutil.DedupeStrSlice(extraHosts) + for _, host := range extraHosts { + if _, err := opts2.ValidateExtraHost(host); err != nil { + return nil, nil, err + } + } + internalLabels.ExtraHosts = extraHosts + + ilOpt, err := common.WithInternalLabels(internalLabels) + if err != nil { + return nil, nil, err + } + cOpts = append(cOpts, ilOpt) + + opts = append(opts, PropagateContainerdLabelsToOCIAnnotations()) + + var s specs.Spec + spec := containerd.WithSpec(&s, opts...) + cOpts = append(cOpts, spec) + + container, err := client.NewContainer(ctx, id, cOpts...) + if err != nil { + gcContainer := func() { + var isErr bool + if errE := os.RemoveAll(stateDir); errE != nil { + isErr = true + } + if name != "" { + var errE error + if containerNameStore, errE = namestore.New(dataStore, ns); errE != nil { + isErr = true + } + if errE = containerNameStore.Release(name, id); errE != nil { + isErr = true + } + + } + if isErr { + logrus.Warnf("failed to remove container %q", id) + } + } + return nil, gcContainer, err + } + return container, nil, nil +} + +func GenerateRootfsOpts(ctx context.Context, client *containerd.Client, platform string, cmd *cobra.Command, args []string, id string) ([]oci.SpecOpts, []containerd.NewContainerOpts, *imgutil.EnsuredImage, error) { + var ( + ensured *imgutil.EnsuredImage + err error + ) + imageless, err := cmd.Flags().GetBool("rootfs") + if err != nil { + return nil, nil, nil, err + } + if !imageless { + pull, err := cmd.Flags().GetString("pull") + if err != nil { + return nil, nil, nil, err + } + var platformSS []string // len: 0 or 1 + if platform != "" { + platformSS = append(platformSS, platform) + } + ocispecPlatforms, err := platformutil.NewOCISpecPlatformSlice(false, platformSS) + if err != nil { + return nil, nil, nil, err + } + rawRef := args[0] + ensured, err = utils.EnsureImage(ctx, cmd, client, rawRef, ocispecPlatforms, pull, nil, false) + if err != nil { + return nil, nil, nil, err + } + } + var ( + opts []oci.SpecOpts + cOpts []containerd.NewContainerOpts + ) + if !imageless { + cOpts = append(cOpts, + containerd.WithImage(ensured.Image), + containerd.WithSnapshotter(ensured.Snapshotter), + containerd.WithNewSnapshot(id, ensured.Image), + containerd.WithImageStopSignal(ensured.Image, "SIGTERM"), + ) + + if len(ensured.ImageConfig.Env) == 0 { + opts = append(opts, oci.WithDefaultPathEnv) + } + for ind, env := range ensured.ImageConfig.Env { + if strings.HasPrefix(env, "PATH=") { + break + } else { + if ind == len(ensured.ImageConfig.Env)-1 { + opts = append(opts, oci.WithDefaultPathEnv) + } + } + } + } else { + absRootfs, err := filepath.Abs(args[0]) + if err != nil { + return nil, nil, nil, err + } + opts = append(opts, oci.WithRootFSPath(absRootfs), oci.WithDefaultPathEnv) + } + + // NOTE: "--entrypoint" can be set to an empty string, see TestRunEntrypoint* in run_test.go . + entrypoint, err := cmd.Flags().GetStringArray("entrypoint") + if err != nil { + return nil, nil, nil, err + } + + if !imageless && !cmd.Flag("entrypoint").Changed { + opts = append(opts, oci.WithImageConfigArgs(ensured.Image, args[1:])) + } else { + if !imageless { + opts = append(opts, oci.WithImageConfig(ensured.Image)) + } + var processArgs []string + if len(entrypoint) != 0 { + processArgs = append(processArgs, entrypoint...) + } + if len(args) > 1 { + processArgs = append(processArgs, args[1:]...) + } + if len(processArgs) == 0 { + // error message is from Podman + return nil, nil, nil, errors.New("no command or entrypoint provided, and no CMD or ENTRYPOINT from image") + } + opts = append(opts, oci.WithProcessArgs(processArgs...)) + } + + initProcessFlag, err := cmd.Flags().GetBool("init") + if err != nil { + return nil, nil, nil, err + } + initBinary, err := cmd.Flags().GetString("init-binary") + if err != nil { + return nil, nil, nil, err + } + if cmd.Flags().Changed("init-binary") { + initProcessFlag = true + } + if initProcessFlag { + binaryPath, err := exec.LookPath(initBinary) + if err != nil { + if errors.Is(err, exec.ErrNotFound) { + return nil, nil, nil, fmt.Errorf(`init binary %q not found`, initBinary) + } + return nil, nil, nil, err + } + inContainerPath := filepath.Join("/sbin", filepath.Base(initBinary)) + opts = append(opts, func(_ context.Context, _ oci.Client, _ *containers.Container, spec *oci.Spec) error { + spec.Process.Args = append([]string{inContainerPath, "--"}, spec.Process.Args...) + spec.Mounts = append([]specs.Mount{{ + Destination: inContainerPath, + Type: "bind", + Source: binaryPath, + Options: []string{"bind", "ro"}, + }}, spec.Mounts...) + return nil + }) + } + + readonly, err := cmd.Flags().GetBool("read-only") + if err != nil { + return nil, nil, nil, err + } + if readonly { + opts = append(opts, oci.WithRootFSReadonly()) + } + return opts, cOpts, ensured, nil +} + +func GenerateLogURI(dataStore string) (*url.URL, error) { + selfExe, err := os.Executable() + if err != nil { + return nil, err + } + args := map[string]string{ + logging.MagicArgv1: dataStore, + } + + return cio.LogURIGenerator("binary", selfExe, args) +} + +func WithNerdctlOCIHook(cmd *cobra.Command, id string) (oci.SpecOpts, error) { + selfExe, f := utils.GlobalFlags(cmd) + args := append([]string{selfExe}, append(f, "internal", "oci-hook")...) + return func(_ context.Context, _ oci.Client, _ *containers.Container, s *specs.Spec) error { + if s.Hooks == nil { + s.Hooks = &specs.Hooks{} + } + crArgs := append(args, "createRuntime") + s.Hooks.CreateRuntime = append(s.Hooks.CreateRuntime, specs.Hook{ + Path: selfExe, + Args: crArgs, + Env: os.Environ(), + }) + argsCopy := append([]string(nil), args...) + psArgs := append(argsCopy, "postStop") + s.Hooks.Poststop = append(s.Hooks.Poststop, specs.Hook{ + Path: selfExe, + Args: psArgs, + Env: os.Environ(), + }) + return nil + }, nil +} + +func WithContainerLabels(cmd *cobra.Command) ([]containerd.NewContainerOpts, error) { + labelMap, err := common.ReadKVStringsMapfFromLabel(cmd) + if err != nil { + return nil, err + } + o := containerd.WithAdditionalContainerLabels(labelMap) + return []containerd.NewContainerOpts{o}, nil +} + +// parseKVStringsMapFromLogOpt parse log options KV entries and convert to Map +func ParseKVStringsMapFromLogOpt(cmd *cobra.Command, logDriver string) (map[string]string, error) { + logOptArray, err := cmd.Flags().GetStringArray("log-opt") + if err != nil { + return nil, err + } + logOptArray = strutil.DedupeStrSlice(logOptArray) + logOptMap := strutil.ConvertKVStringsToMap(logOptArray) + if logDriver == "json-file" { + if _, ok := logOptMap[logging.MaxSize]; !ok { + delete(logOptMap, logging.MaxFile) + } + } + if err := logging.ValidateLogOpts(logDriver, logOptMap); err != nil { + return nil, err + } + return logOptMap, nil +} + +func WithStop(stopSignal string, stopTimeout int, ensuredImage *imgutil.EnsuredImage) containerd.NewContainerOpts { + return func(ctx context.Context, _ *containerd.Client, c *containers.Container) error { + if c.Labels == nil { + c.Labels = make(map[string]string) + } + var err error + if ensuredImage != nil { + stopSignal, err = containerd.GetOCIStopSignal(ctx, ensuredImage.Image, stopSignal) + if err != nil { + return err + } + } + c.Labels[containerd.StopSignalLabel] = stopSignal + if stopTimeout != 0 { + c.Labels[labels.StopTimout] = strconv.Itoa(stopTimeout) + } + return nil + } +} + +func PropagateContainerdLabelsToOCIAnnotations() oci.SpecOpts { + return func(ctx context.Context, oc oci.Client, c *containers.Container, s *oci.Spec) error { + return oci.WithAnnotations(c.Labels)(ctx, oc, c, s) + } +} + +func WriteCIDFile(path, id string) error { + if _, err := os.Stat(path); err == nil { + return fmt.Errorf("container ID file found, make sure the other container isn't running or delete %s", path) + } else if errors.Is(err, os.ErrNotExist) { + f, err := os.Create(path) + if err != nil { + return err + } + defer f.Close() + if err != nil { + return fmt.Errorf("failed to create the container ID file: %s", err) + } + if _, err := f.WriteString(id); err != nil { + return err + } + return nil + } else { + return err + } +} + +func ParseEnvVars(paths []string) ([]string, error) { + vars := make([]string, 0) + for _, path := range paths { + f, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("failed to open env file %s: %w", path, err) + } + defer f.Close() + + sc := bufio.NewScanner(f) + for sc.Scan() { + line := strings.TrimSpace(sc.Text()) + // skip comment lines + if strings.HasPrefix(line, "#") { + continue + } + vars = append(vars, line) + } + if err = sc.Err(); err != nil { + return nil, err + } + } + return vars, nil +} + +func WithOSEnv(envs []string) ([]string, error) { + newEnvs := make([]string, len(envs)) + + // from https://github.com/docker/cli/blob/v22.06.0-beta.0/opts/env.go#L18 + getEnv := func(val string) (string, error) { + arr := strings.SplitN(val, "=", 2) + if arr[0] == "" { + return "", errors.New("invalid environment variable: " + val) + } + if len(arr) > 1 { + return val, nil + } + if envVal, ok := os.LookupEnv(arr[0]); ok { + return arr[0] + "=" + envVal, nil + } + return val, nil + } + for i := range envs { + env, err := getEnv(envs[i]) + if err != nil { + return nil, err + } + newEnvs[i] = env + } + + return newEnvs, nil +} + +// generateEnvs combines environment variables from `--env-file` and `--env`. +// Pass an empty slice if any arg is not used. +func GenerateEnvs(envFile []string, env []string) ([]string, error) { + var envs []string + var err error + + if envFiles := strutil.DedupeStrSlice(envFile); len(envFiles) > 0 { + envs, err = ParseEnvVars(envFiles) + if err != nil { + return nil, err + } + } + + if env := strutil.DedupeStrSlice(env); len(env) > 0 { + envs = append(envs, env...) + } + + if envs, err = WithOSEnv(envs); err != nil { + return nil, err + } + + return envs, nil +} diff --git a/cmd/nerdctl/run_mount.go b/cmd/nerdctl/utils/action/run/mount.go similarity index 98% rename from cmd/nerdctl/run_mount.go rename to cmd/nerdctl/utils/action/run/mount.go index d9837a131ea..843bb1d146f 100644 --- a/cmd/nerdctl/run_mount.go +++ b/cmd/nerdctl/utils/action/run/mount.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package run import ( "context" @@ -30,6 +30,7 @@ import ( "github.com/containerd/containerd/mount" "github.com/containerd/containerd/oci" "github.com/containerd/continuity/fs" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/volume" "github.com/containerd/nerdctl/pkg/idgen" "github.com/containerd/nerdctl/pkg/imgutil" "github.com/containerd/nerdctl/pkg/mountutil" @@ -38,7 +39,6 @@ import ( securejoin "github.com/cyphar/filepath-securejoin" "github.com/opencontainers/image-spec/identity" "github.com/opencontainers/runtime-spec/specs-go" - "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -120,8 +120,8 @@ func parseMountFlags(cmd *cobra.Command, volStore volumestore.VolumeStore) ([]*m // generateMountOpts generates volume-related mount opts. // Other mounts such as procfs mount are not handled here. -func generateMountOpts(ctx context.Context, cmd *cobra.Command, client *containerd.Client, ensuredImage *imgutil.EnsuredImage) ([]oci.SpecOpts, []string, []*mountutil.Processed, error) { - volStore, err := getVolumeStore(cmd) +func GenerateMountOpts(ctx context.Context, cmd *cobra.Command, client *containerd.Client, ensuredImage *imgutil.EnsuredImage) ([]oci.SpecOpts, []string, []*mountutil.Processed, error) { + volStore, err := volume.Store(cmd) if err != nil { return nil, nil, nil, err } diff --git a/cmd/nerdctl/utils/action/run/run.go b/cmd/nerdctl/utils/action/run/run.go new file mode 100644 index 00000000000..3fff372da4d --- /dev/null +++ b/cmd/nerdctl/utils/action/run/run.go @@ -0,0 +1,315 @@ +/* + 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 run + +import ( + "context" + "fmt" + "path" + "strings" + + "github.com/containerd/containerd" + "github.com/containerd/containerd/containers" + "github.com/containerd/containerd/oci" + "github.com/containerd/nerdctl/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/common" + "github.com/containerd/nerdctl/pkg/defaults" + "github.com/containerd/nerdctl/pkg/labels" + "github.com/containerd/nerdctl/pkg/logging" + "github.com/containerd/nerdctl/pkg/netutil" + "github.com/containerd/nerdctl/pkg/rootlessutil" + "github.com/opencontainers/runtime-spec/specs-go" + "github.com/spf13/cobra" +) + +func SetCreateFlags(cmd *cobra.Command) { + + // No "-h" alias for "--help", because "-h" for "--hostname". + cmd.Flags().Bool("help", false, "show help") + + cmd.Flags().BoolP("tty", "t", false, "(Currently -t needs to correspond to -i)") + cmd.Flags().BoolP("interactive", "i", false, "Keep STDIN open even if not attached") + cmd.Flags().String("restart", "no", `Restart policy to apply when a container exits (implemented values: "no"|"always")`) + cmd.RegisterFlagCompletionFunc("restart", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{"no", "always", "on-failure", "unless-stopped"}, cobra.ShellCompDirectiveNoFileComp + }) + cmd.Flags().Bool("rm", false, "Automatically remove the container when it exits") + cmd.Flags().String("pull", "missing", `Pull image before running ("always"|"missing"|"never")`) + cmd.RegisterFlagCompletionFunc("pull", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{"always", "missing", "never"}, cobra.ShellCompDirectiveNoFileComp + }) + cmd.Flags().String("stop-signal", "SIGTERM", "Signal to stop a container") + cmd.Flags().Int("stop-timeout", 0, "Timeout (in seconds) to stop a container") + + // #region for init process + cmd.Flags().Bool("init", false, "Run an init process inside the container, Default to use tini") + cmd.Flags().String("init-binary", common.TiniInitBinary, "The custom binary to use as the init process") + // #endregion + + // #region platform flags + cmd.Flags().String("platform", "", "Set platform (e.g. \"amd64\", \"arm64\")") // not a slice, and there is no --all-platforms + cmd.RegisterFlagCompletionFunc("platform", completion.ShellCompletePlatforms) + // #endregion + + // #region network flags + // network (net) is defined as StringSlice, not StringArray, to allow specifying "--network=cni1,cni2" + cmd.Flags().StringSlice("network", []string{netutil.DefaultNetworkName}, `Connect a container to a network ("bridge"|"host"|"none"|"container:"|)`) + cmd.RegisterFlagCompletionFunc("network", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return completion.ShellCompleteNetworkNames(cmd, []string{}) + }) + cmd.Flags().StringSlice("net", []string{netutil.DefaultNetworkName}, `Connect a container to a network ("bridge"|"host"|"none"|)`) + cmd.RegisterFlagCompletionFunc("net", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return completion.ShellCompleteNetworkNames(cmd, []string{}) + }) + // dns is defined as StringSlice, not StringArray, to allow specifying "--dns=1.1.1.1,8.8.8.8" (compatible with Podman) + cmd.Flags().StringSlice("dns", nil, "Set custom DNS servers") + cmd.Flags().StringSlice("dns-search", nil, "Set custom DNS search domains") + // We allow for both "--dns-opt" and "--dns-option", although the latter is the recommended way. + cmd.Flags().StringSlice("dns-opt", nil, "Set DNS options") + cmd.Flags().StringSlice("dns-option", nil, "Set DNS options") + // publish is defined as StringSlice, not StringArray, to allow specifying "--publish=80:80,443:443" (compatible with Podman) + cmd.Flags().StringSliceP("publish", "p", nil, "Publish a container's port(s) to the host") + // FIXME: not support IPV6 yet + cmd.Flags().String("ip", "", "IPv4 address to assign to the container") + cmd.Flags().StringP("hostname", "h", "", "Container host name") + cmd.Flags().String("mac-address", "", "MAC address to assign to the container") + // #endregion + + cmd.Flags().String("ipc", "", `IPC namespace to use ("host"|"private")`) + cmd.RegisterFlagCompletionFunc("ipc", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{"host", "private"}, cobra.ShellCompDirectiveNoFileComp + }) + // #region cgroups, namespaces, and ulimits flags + cmd.Flags().Float64("cpus", 0.0, "Number of CPUs") + cmd.Flags().StringP("memory", "m", "", "Memory limit") + cmd.Flags().String("memory-reservation", "", "Memory soft limit") + cmd.Flags().String("memory-swap", "", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap") + cmd.Flags().Int64("memory-swappiness", -1, "Tune container memory swappiness (0 to 100) (default -1)") + cmd.Flags().String("kernel-memory", "", "Kernel memory limit (deprecated)") + cmd.Flags().Bool("oom-kill-disable", false, "Disable OOM Killer") + cmd.Flags().Int("oom-score-adj", 0, "Tune container’s OOM preferences (-1000 to 1000, rootless: 100 to 1000)") + cmd.Flags().String("pid", "", "PID namespace to use") + cmd.RegisterFlagCompletionFunc("pid", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{"host"}, cobra.ShellCompDirectiveNoFileComp + }) + cmd.Flags().Int64("pids-limit", -1, "Tune container pids limit (set -1 for unlimited)") + cmd.Flags().StringSlice("cgroup-conf", nil, "Configure cgroup v2 (key=value)") + cmd.Flags().Uint16("blkio-weight", 0, "Block IO (relative weight), between 10 and 1000, or 0 to disable (default 0)") + cmd.Flags().String("cgroupns", defaults.CgroupnsMode(), `Cgroup namespace to use, the default depends on the cgroup version ("host"|"private")`) + cmd.RegisterFlagCompletionFunc("cgroupns", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{"host", "private"}, cobra.ShellCompDirectiveNoFileComp + }) + cmd.Flags().String("cpuset-cpus", "", "CPUs in which to allow execution (0-3, 0,1)") + cmd.Flags().String("cpuset-mems", "", "MEMs in which to allow execution (0-3, 0,1)") + cmd.Flags().Uint64("cpu-shares", 0, "CPU shares (relative weight)") + cmd.Flags().Int64("cpu-quota", -1, "Limit CPU CFS (Completely Fair Scheduler) quota") + cmd.Flags().Uint64("cpu-period", 0, "Limit CPU CFS (Completely Fair Scheduler) period") + // device is defined as StringSlice, not StringArray, to allow specifying "--device=DEV1,DEV2" (compatible with Podman) + cmd.Flags().StringSlice("device", nil, "Add a host device to the container") + // ulimit is defined as StringSlice, not StringArray, to allow specifying "--ulimit=ULIMIT1,ULIMIT2" (compatible with Podman) + cmd.Flags().StringSlice("ulimit", nil, "Ulimit options") + cmd.Flags().String("rdt-class", "", "Name of the RDT class (or CLOS) to associate the container with") + // #endregion + + // user flags + cmd.Flags().StringP("user", "u", "", "Username or UID (format: [:])") + cmd.Flags().String("umask", "", "Set the umask inside the container. Defaults to 0022") + cmd.Flags().StringSlice("group-add", []string{}, "Add additional groups to join") + + // #region security flags + cmd.Flags().StringArray("security-opt", []string{}, "Security options") + cmd.RegisterFlagCompletionFunc("security-opt", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{"seccomp=", "seccomp=unconfined", "apparmor=", "apparmor=" + defaults.AppArmorProfileName, "apparmor=unconfined", "no-new-privileges", "privileged-without-host-devices"}, cobra.ShellCompDirectiveNoFileComp + }) + // cap-add and cap-drop are defined as StringSlice, not StringArray, to allow specifying "--cap-add=CAP_SYS_ADMIN,CAP_NET_ADMIN" (compatible with Podman) + cmd.Flags().StringSlice("cap-add", []string{}, "Add Linux Capabilities") + cmd.RegisterFlagCompletionFunc("cap-add", completion.CapShellComplete) + cmd.Flags().StringSlice("cap-drop", []string{}, "Drop Linux Capabilities") + cmd.RegisterFlagCompletionFunc("cap-drop", completion.CapShellComplete) + cmd.Flags().Bool("privileged", false, "Give extended privileges to this container") + // #endregion + + // #region runtime flags + cmd.Flags().String("runtime", defaults.Runtime, "Runtime to use for this container, e.g. \"crun\", or \"io.containerd.runsc.v1\"") + // sysctl needs to be StringArray, not StringSlice, to prevent "foo=foo1,foo2" from being split to {"foo=foo1", "foo2"} + cmd.Flags().StringArray("sysctl", nil, "Sysctl options") + // gpus needs to be StringArray, not StringSlice, to prevent "Capabilities=utility,device=DEV" from being split to {"Capabilities=utility", "device=DEV"} + cmd.Flags().StringArray("gpus", nil, "GPU devices to Add to the container ('all' to pass all GPUs)") + cmd.RegisterFlagCompletionFunc("gpus", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{"all"}, cobra.ShellCompDirectiveNoFileComp + }) + // #endregion + + // #region mount flags + // volume needs to be StringArray, not StringSlice, to prevent "/foo:/foo:ro,Z" from being split to {"/foo:/foo:ro", "Z"} + cmd.Flags().StringArrayP("volume", "v", nil, "Bind mount a volume") + // tmpfs needs to be StringArray, not StringSlice, to prevent "/foo:size=64m,exec" from being split to {"/foo:size=64m", "exec"} + cmd.Flags().StringArray("tmpfs", nil, "Mount a tmpfs directory") + cmd.Flags().StringArray("mount", nil, "Attach a filesystem mount to the container") + // #endregion + + // rootfs flags + cmd.Flags().Bool("read-only", false, "Mount the container's root filesystem as read only") + // rootfs flags (from Podman) + cmd.Flags().Bool("rootfs", false, "The first argument is not an image but the rootfs to the exploded container") + + // #region env flags + // entrypoint needs to be StringArray, not StringSlice, to prevent "FOO=foo1,foo2" from being split to {"FOO=foo1", "foo2"} + // entrypoint StringArray is an internal implementation to support `nerdctl compose` entrypoint yaml filed with multiple strings + // users are not expected to specify multiple --entrypoint flags manually. + cmd.Flags().StringArray("entrypoint", nil, "Overwrite the default ENTRYPOINT of the image") + cmd.Flags().StringP("workdir", "w", "", "Working directory inside the container") + // env needs to be StringArray, not StringSlice, to prevent "FOO=foo1,foo2" from being split to {"FOO=foo1", "foo2"} + cmd.Flags().StringArrayP("env", "e", nil, "Set environment variables") + // add-host is defined as StringSlice, not StringArray, to allow specifying "--add-host=HOST1:IP1,HOST2:IP2" (compatible with Podman) + cmd.Flags().StringSlice("add-host", nil, "Add a custom host-to-IP mapping (host:ip)") + // env-file is defined as StringSlice, not StringArray, to allow specifying "--env-file=FILE1,FILE2" (compatible with Podman) + cmd.Flags().StringSlice("env-file", nil, "Set environment variables from file") + + // #region metadata flags + cmd.Flags().String("name", "", "Assign a name to the container") + // label needs to be StringArray, not StringSlice, to prevent "foo=foo1,foo2" from being split to {"foo=foo1", "foo2"} + cmd.Flags().StringArrayP("label", "l", nil, "Set metadata on container") + cmd.RegisterFlagCompletionFunc("label", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return labels.ShellCompletions, cobra.ShellCompDirectiveNoFileComp + }) + + // label-file is defined as StringSlice, not StringArray, to allow specifying "--env-file=FILE1,FILE2" (compatible with Podman) + cmd.Flags().StringSlice("label-file", nil, "Set metadata on container from file") + cmd.Flags().String("cidfile", "", "Write the container ID to the file") + // #endregion + + // #region logging flags + // log-opt needs to be StringArray, not StringSlice, to prevent "env=os,customer" from being split to {"env=os", "customer"} + cmd.Flags().String("log-driver", "json-file", "Logging driver for the container. Default is json-file. It also supports logURI (eg: --log-driver binary://)") + cmd.RegisterFlagCompletionFunc("log-driver", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return logging.Drivers(), cobra.ShellCompDirectiveNoFileComp + }) + cmd.Flags().StringArray("log-opt", nil, "Log driver options") + // #endregion + + // shared memory flags + cmd.Flags().String("shm-size", "", "Size of /dev/shm") + cmd.Flags().String("pidfile", "", "file path to write the task's pid") + + // #region verify flags + cmd.Flags().String("verify", "none", "Verify the image (none|cosign)") + cmd.RegisterFlagCompletionFunc("verify", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{"none", "cosign"}, cobra.ShellCompDirectiveNoFileComp + }) + cmd.Flags().String("cosign-key", "", "Path to the public key file, KMS, URI or Kubernetes Secret for --verify=cosign") + // #endregion +} + +// WithBindMountHostIPC replaces /dev/shm and /dev/mqueue mount with rbind. +// Required for --ipc=host on rootless. +func WithBindMountHostIPC(_ context.Context, _ oci.Client, _ *containers.Container, s *oci.Spec) error { + for i, m := range s.Mounts { + if path.Clean(m.Destination) == "/dev/shm" { + newM := specs.Mount{ + Destination: "/dev/shm", + Type: "bind", + Source: "/dev/shm", + Options: []string{"rbind", "nosuid", "noexec", "nodev"}, + } + s.Mounts[i] = newM + } + if path.Clean(m.Destination) == "/dev/mqueue" { + newM := specs.Mount{ + Destination: "/dev/mqueue", + Type: "bind", + Source: "/dev/mqueue", + Options: []string{"rbind", "nosuid", "noexec", "nodev"}, + } + s.Mounts[i] = newM + } + } + return nil +} + +// withBindMountHostProcfs replaces procfs mount with rbind. +// Required for --pid=host on rootless. +// +// https://github.com/moby/moby/pull/41893/files +// https://github.com/containers/podman/blob/v3.0.0-rc1/pkg/specgen/generate/oci.go#L248-L257 +func WithBindMountHostProcfs(_ context.Context, _ oci.Client, _ *containers.Container, s *oci.Spec) error { + for i, m := range s.Mounts { + if path.Clean(m.Destination) == "/proc" { + newM := specs.Mount{ + Destination: "/proc", + Type: "bind", + Source: "/proc", + Options: []string{"rbind", "nosuid", "noexec", "nodev"}, + } + s.Mounts[i] = newM + } + } + + // Remove ReadonlyPaths for /proc/* + newROP := s.Linux.ReadonlyPaths[:0] + for _, x := range s.Linux.ReadonlyPaths { + x = path.Clean(x) + if !strings.HasPrefix(x, "/proc/") { + newROP = append(newROP, x) + } + } + s.Linux.ReadonlyPaths = newROP + return nil +} + +func GenerateSharingPIDOpts(ctx context.Context, targetCon containerd.Container) ([]oci.SpecOpts, error) { + opts := make([]oci.SpecOpts, 0) + + task, err := targetCon.Task(ctx, nil) + if err != nil { + return nil, err + } + status, err := task.Status(ctx) + if err != nil { + return nil, err + } + + if status.Status != containerd.Running { + return nil, fmt.Errorf("shared container is not running") + } + + spec, err := targetCon.Spec(ctx) + if err != nil { + return nil, err + } + + isHost := true + for _, n := range spec.Linux.Namespaces { + if n.Type == specs.PIDNamespace { + isHost = false + } + } + if isHost { + opts = append(opts, oci.WithHostNamespace(specs.PIDNamespace)) + if rootlessutil.IsRootless() { + opts = append(opts, WithBindMountHostProcfs) + } + } else { + ns := specs.LinuxNamespace{ + Type: specs.PIDNamespace, + Path: fmt.Sprintf("/proc/%d/ns/pid", task.Pid()), + } + opts = append(opts, oci.WithLinuxNamespace(ns)) + } + + return opts, nil +} diff --git a/cmd/nerdctl/run_cgroup_freebsd.go b/cmd/nerdctl/utils/action/run/run_cgroup_freebsd.go similarity index 98% rename from cmd/nerdctl/run_cgroup_freebsd.go rename to cmd/nerdctl/utils/action/run/run_cgroup_freebsd.go index bb0fe0f6213..37fcddd1de8 100644 --- a/cmd/nerdctl/run_cgroup_freebsd.go +++ b/cmd/nerdctl/utils/action/run/run_cgroup_freebsd.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package run import ( "github.com/containerd/containerd/oci" diff --git a/cmd/nerdctl/run_cgroup_linux.go b/cmd/nerdctl/utils/action/run/run_cgroup_linux.go similarity index 98% rename from cmd/nerdctl/run_cgroup_linux.go rename to cmd/nerdctl/utils/action/run/run_cgroup_linux.go index 2c964f46c0a..a30620508b6 100644 --- a/cmd/nerdctl/run_cgroup_linux.go +++ b/cmd/nerdctl/utils/action/run/run_cgroup_linux.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package run import ( "context" @@ -271,7 +271,7 @@ func generateCgroupOpts(cmd *cobra.Command, id string) ([]oci.SpecOpts, error) { return nil, err } for _, f := range device { - devPath, mode, err := parseDevice(f) + devPath, mode, err := ParseDevice(f) if err != nil { return nil, fmt.Errorf("failed to parse device %q: %w", f, err) } @@ -280,7 +280,7 @@ func generateCgroupOpts(cmd *cobra.Command, id string) ([]oci.SpecOpts, error) { return opts, nil } -func parseDevice(s string) (hostDevPath string, mode string, err error) { +func ParseDevice(s string) (hostDevPath string, mode string, err error) { mode = "rwm" split := strings.Split(s, ":") var containerDevPath string diff --git a/cmd/nerdctl/run_cgroup_windows.go b/cmd/nerdctl/utils/action/run/run_cgroup_windows.go similarity index 98% rename from cmd/nerdctl/run_cgroup_windows.go rename to cmd/nerdctl/utils/action/run/run_cgroup_windows.go index bb0fe0f6213..37fcddd1de8 100644 --- a/cmd/nerdctl/run_cgroup_windows.go +++ b/cmd/nerdctl/utils/action/run/run_cgroup_windows.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package run import ( "github.com/containerd/containerd/oci" diff --git a/cmd/nerdctl/run_freebsd.go b/cmd/nerdctl/utils/action/run/run_freebsd.go similarity index 68% rename from cmd/nerdctl/run_freebsd.go rename to cmd/nerdctl/utils/action/run/run_freebsd.go index eef282e940c..53116ade585 100644 --- a/cmd/nerdctl/run_freebsd.go +++ b/cmd/nerdctl/utils/action/run/run_freebsd.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package run import ( "context" @@ -22,23 +22,15 @@ import ( "github.com/containerd/containerd" "github.com/containerd/containerd/containers" "github.com/containerd/containerd/oci" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/common" "github.com/spf13/cobra" ) +func SetPlatformOptions(ctx context.Context, opts []oci.SpecOpts, cmd *cobra.Command, client *containerd.Client, id string, internalLabels common.InternalLabels) ([]oci.SpecOpts, common.InternalLabels, error) { + return opts, internalLabels, nil +} + func WithoutRunMount() func(ctx context.Context, client oci.Client, c *containers.Container, s *oci.Spec) error { // not valid on freebsd return func(_ context.Context, _ oci.Client, _ *containers.Container, s *oci.Spec) error { return nil } } - -func capShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - candidates := []string{} - return candidates, cobra.ShellCompDirectiveNoFileComp -} - -func runShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return nil, cobra.ShellCompDirectiveNoFileComp -} - -func setPlatformOptions(ctx context.Context, opts []oci.SpecOpts, cmd *cobra.Command, client *containerd.Client, id string, internalLabels internalLabels) ([]oci.SpecOpts, internalLabels, error) { - return opts, internalLabels, nil -} diff --git a/cmd/nerdctl/run_gpus.go b/cmd/nerdctl/utils/action/run/run_gpus.go similarity index 83% rename from cmd/nerdctl/run_gpus.go rename to cmd/nerdctl/utils/action/run/run_gpus.go index 7b656fc6d64..2c3e23855fc 100644 --- a/cmd/nerdctl/run_gpus.go +++ b/cmd/nerdctl/utils/action/run/run_gpus.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package run import ( "encoding/csv" @@ -28,10 +28,10 @@ import ( "github.com/containerd/nerdctl/pkg/rootlessutil" ) -type gpuReq struct { - count int - deviceIDs []string - capabilities []string +type GpuReq struct { + Count int + DeviceIDs []string + Capabilities []string } func parseGPUOpts(value []string) (res []oci.SpecOpts, _ error) { @@ -46,22 +46,22 @@ func parseGPUOpts(value []string) (res []oci.SpecOpts, _ error) { } func parseGPUOpt(value string) (oci.SpecOpts, error) { - req, err := parseGPUOptCSV(value) + req, err := ParseGPUOptCSV(value) if err != nil { return nil, err } var gpuOpts []nvidia.Opts - if len(req.deviceIDs) > 0 { - gpuOpts = append(gpuOpts, nvidia.WithDeviceUUIDs(req.deviceIDs...)) - } else if req.count > 0 { + if len(req.DeviceIDs) > 0 { + gpuOpts = append(gpuOpts, nvidia.WithDeviceUUIDs(req.DeviceIDs...)) + } else if req.Count > 0 { var devices []int - for i := 0; i < req.count; i++ { + for i := 0; i < req.Count; i++ { devices = append(devices, i) } gpuOpts = append(gpuOpts, nvidia.WithDevices(devices...)) - } else if req.count < 0 { + } else if req.Count < 0 { gpuOpts = append(gpuOpts, nvidia.WithAllDevices) } @@ -70,7 +70,7 @@ func parseGPUOpt(value string) (oci.SpecOpts, error) { str2cap[string(c)] = c } var nvidiaCaps []nvidia.Capability - for _, c := range req.capabilities { + for _, c := range req.Capabilities { if cap, isNvidiaCap := str2cap[c]; isNvidiaCap { nvidiaCaps = append(nvidiaCaps, cap) } @@ -92,7 +92,7 @@ func parseGPUOpt(value string) (oci.SpecOpts, error) { return nvidia.WithGPUs(gpuOpts...), nil } -func parseGPUOptCSV(value string) (*gpuReq, error) { +func ParseGPUOptCSV(value string) (*GpuReq, error) { csvReader := csv.NewReader(strings.NewReader(value)) fields, err := csvReader.Read() if err != nil { @@ -100,7 +100,7 @@ func parseGPUOptCSV(value string) (*gpuReq, error) { } var ( - req gpuReq + req GpuReq seen = map[string]struct{}{} ) for _, field := range fields { @@ -113,7 +113,7 @@ func parseGPUOptCSV(value string) (*gpuReq, error) { if len(parts) == 1 { seen["count"] = struct{}{} - req.count, err = parseCount(key) + req.Count, err = parseCount(key) if err != nil { return nil, err } @@ -127,14 +127,14 @@ func parseGPUOptCSV(value string) (*gpuReq, error) { return nil, fmt.Errorf("invalid driver %q: \"nvidia\" is only supported", value) } case "count": - req.count, err = parseCount(value) + req.Count, err = parseCount(value) if err != nil { return nil, err } case "device": - req.deviceIDs = strings.Split(value, ",") + req.DeviceIDs = strings.Split(value, ",") case "capabilities": - req.capabilities = strings.Split(value, ",") + req.Capabilities = strings.Split(value, ",") case "options": // This option is allowed but not used for gpus. // Please see also: https://github.com/moby/moby/pull/38828 @@ -143,11 +143,11 @@ func parseGPUOptCSV(value string) (*gpuReq, error) { } } - if req.count != 0 && len(req.deviceIDs) > 0 { + if req.Count != 0 && len(req.DeviceIDs) > 0 { return nil, errors.New("cannot set both Count and DeviceIDs on device request") } - if _, ok := seen["count"]; !ok && len(req.deviceIDs) == 0 { - req.count = 1 + if _, ok := seen["count"]; !ok && len(req.DeviceIDs) == 0 { + req.Count = 1 } return &req, nil diff --git a/cmd/nerdctl/run_linux.go b/cmd/nerdctl/utils/action/run/run_linux.go similarity index 87% rename from cmd/nerdctl/run_linux.go rename to cmd/nerdctl/utils/action/run/run_linux.go index 94b4b2938a0..1b09c2387e2 100644 --- a/cmd/nerdctl/run_linux.go +++ b/cmd/nerdctl/utils/action/run/run_linux.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package run import ( "context" @@ -24,8 +24,8 @@ import ( "github.com/containerd/containerd" "github.com/containerd/containerd/containers" "github.com/containerd/containerd/oci" - "github.com/containerd/containerd/pkg/cap" "github.com/containerd/containerd/pkg/userns" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/common" "github.com/containerd/nerdctl/pkg/bypass4netnsutil" "github.com/containerd/nerdctl/pkg/idutil/containerwalker" "github.com/containerd/nerdctl/pkg/rootlessutil" @@ -36,28 +36,7 @@ import ( "github.com/spf13/cobra" ) -func WithoutRunMount() func(ctx context.Context, client oci.Client, c *containers.Container, s *oci.Spec) error { - return oci.WithoutRunMount -} - -func capShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - candidates := []string{} - for _, c := range cap.Known() { - // "CAP_SYS_ADMIN" -> "sys_admin" - s := strings.ToLower(strings.TrimPrefix(c, "CAP_")) - candidates = append(candidates, s) - } - return candidates, cobra.ShellCompDirectiveNoFileComp -} - -func runShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if len(args) == 0 { - return shellCompleteImageNames(cmd) - } - return nil, cobra.ShellCompDirectiveNoFileComp -} - -func setPlatformOptions(ctx context.Context, opts []oci.SpecOpts, cmd *cobra.Command, client *containerd.Client, id string, internalLabels internalLabels) ([]oci.SpecOpts, internalLabels, error) { +func SetPlatformOptions(ctx context.Context, opts []oci.SpecOpts, cmd *cobra.Command, client *containerd.Client, id string, internalLabels common.InternalLabels) ([]oci.SpecOpts, common.InternalLabels, error) { opts = append(opts, oci.WithDefaultUnixDevices, WithoutRunMount(), // unmount default tmpfs on "/run": https://github.com/containerd/nerdctl/issues/157) @@ -74,7 +53,7 @@ func setPlatformOptions(ctx context.Context, opts []oci.SpecOpts, cmd *cobra.Com } opts = append(opts, cgOpts...) - labelsMap, err := readKVStringsMapfFromLabel(cmd) + labelsMap, err := common.ReadKVStringsMapfFromLabel(cmd) if err != nil { return nil, internalLabels, err } @@ -137,7 +116,7 @@ func setPlatformOptions(ctx context.Context, opts []oci.SpecOpts, cmd *cobra.Com if err != nil { return nil, internalLabels, err } - internalLabels.pidContainer = pidInternalLabel + internalLabels.PidContainer = pidInternalLabel opts = append(opts, pidOpts...) ulimitOpts, err := generateUlimitsOpts(cmd) @@ -175,7 +154,7 @@ func setPlatformOptions(ctx context.Context, opts []oci.SpecOpts, cmd *cobra.Com // if nothing is specified, or if private, default to normal behavior if ipc == "host" { opts = append(opts, oci.WithHostNamespace(specs.IPCNamespace)) - opts = append(opts, withBindMountHostIPC) + opts = append(opts, WithBindMountHostIPC) } else if ipc != "" && ipc != "private" { return nil, internalLabels, fmt.Errorf("error: %v", "invalid ipc value, supported values are 'private' or 'host'") } @@ -239,7 +218,7 @@ func generatePIDOpts(ctx context.Context, client *containerd.Client, pid string) case "host": opts = append(opts, oci.WithHostNamespace(specs.PIDNamespace)) if rootlessutil.IsRootless() { - opts = append(opts, withBindMountHostProcfs) + opts = append(opts, WithBindMountHostProcfs) } default: // container: parsed := strings.Split(pid, ":") @@ -255,7 +234,7 @@ func generatePIDOpts(ctx context.Context, client *containerd.Client, pid string) return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req) } - o, err := generateSharingPIDOpts(ctx, found.Container) + o, err := GenerateSharingPIDOpts(ctx, found.Container) if err != nil { return err } @@ -276,3 +255,7 @@ func generatePIDOpts(ctx context.Context, client *containerd.Client, pid string) return opts, pidInternalLabel, nil } + +func WithoutRunMount() func(ctx context.Context, client oci.Client, c *containers.Container, s *oci.Spec) error { + return oci.WithoutRunMount +} diff --git a/cmd/nerdctl/run_network.go b/cmd/nerdctl/utils/action/run/run_network.go similarity index 89% rename from cmd/nerdctl/run_network.go rename to cmd/nerdctl/utils/action/run/run_network.go index fe41fa46c3e..214f7dbf5db 100644 --- a/cmd/nerdctl/run_network.go +++ b/cmd/nerdctl/utils/action/run/run_network.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package run import ( "context" @@ -30,6 +30,8 @@ import ( "github.com/containerd/containerd/containers" "github.com/containerd/containerd/oci" gocni "github.com/containerd/go-cni" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/common" "github.com/containerd/nerdctl/pkg/dnsutil" "github.com/containerd/nerdctl/pkg/dnsutil/hostsstore" "github.com/containerd/nerdctl/pkg/idutil/containerwalker" @@ -45,6 +47,18 @@ import ( "github.com/spf13/cobra" ) +func WithCustomEtcHostname(src string) func(context.Context, oci.Client, *containers.Container, *oci.Spec) error { + return func(_ context.Context, _ oci.Client, _ *containers.Container, s *oci.Spec) error { + s.Mounts = append(s.Mounts, specs.Mount{ + Destination: "/etc/hostname", + Type: "bind", + Source: src, + Options: []string{"bind", mountutil.DefaultPropagationMode}, // writable + }) + return nil + } +} + func getNetworkSlice(cmd *cobra.Command) ([]string, error) { var netSlice = []string{} var networkSet = false @@ -75,7 +89,7 @@ func getNetworkSlice(cmd *cobra.Command) ([]string, error) { return netSlice, nil } -func withCustomResolvConf(src string) func(context.Context, oci.Client, *containers.Container, *oci.Spec) error { +func WithCustomResolvConf(src string) func(context.Context, oci.Client, *containers.Container, *oci.Spec) error { return func(_ context.Context, _ oci.Client, _ *containers.Container, s *oci.Spec) error { s.Mounts = append(s.Mounts, specs.Mount{ Destination: "/etc/resolv.conf", @@ -87,19 +101,7 @@ func withCustomResolvConf(src string) func(context.Context, oci.Client, *contain } } -func withCustomEtcHostname(src string) func(context.Context, oci.Client, *containers.Container, *oci.Spec) error { - return func(_ context.Context, _ oci.Client, _ *containers.Container, s *oci.Spec) error { - s.Mounts = append(s.Mounts, specs.Mount{ - Destination: "/etc/hostname", - Type: "bind", - Source: src, - Options: []string{"bind", mountutil.DefaultPropagationMode}, // writable - }) - return nil - } -} - -func withCustomHosts(src string) func(context.Context, oci.Client, *containers.Container, *oci.Spec) error { +func WithCustomHosts(src string) func(context.Context, oci.Client, *containers.Container, *oci.Spec) error { return func(_ context.Context, _ oci.Client, _ *containers.Container, s *oci.Spec) error { s.Mounts = append(s.Mounts, specs.Mount{ Destination: "/etc/hosts", @@ -111,7 +113,7 @@ func withCustomHosts(src string) func(context.Context, oci.Client, *containers.C } } -func generateNetOpts(cmd *cobra.Command, dataStore, stateDir, ns, id string) ([]oci.SpecOpts, []string, string, []gocni.PortMapping, string, error) { +func GenerateNetOpts(cmd *cobra.Command, dataStore, stateDir, ns, id string) ([]oci.SpecOpts, []string, string, []gocni.PortMapping, string, error) { opts := []oci.SpecOpts{} portSlice, err := cmd.Flags().GetStringSlice("publish") if err != nil { @@ -130,7 +132,7 @@ func generateNetOpts(cmd *cobra.Command, dataStore, stateDir, ns, id string) ([] logrus.Warnf("You have assign an IP address %s but no network, So we will use the default network", ipAddress) } - macAddress, err := getMACAddress(cmd, netSlice) + macAddress, err := MACAddress(cmd, netSlice) if err != nil { return nil, nil, "", nil, "", err } @@ -154,13 +156,13 @@ func generateNetOpts(cmd *cobra.Command, dataStore, stateDir, ns, id string) ([] case nettype.CNI: // We only verify flags and generate resolv.conf here. // The actual network is configured in the oci hook. - if err := verifyCNINetwork(cmd, netSlice, macAddress); err != nil { + if err := VerifyCNINetwork(cmd, netSlice, macAddress); err != nil { return nil, nil, "", nil, "", err } if runtime.GOOS == "linux" { resolvConfPath := filepath.Join(stateDir, "resolv.conf") - if err := buildResolvConf(cmd, resolvConfPath); err != nil { + if err := BuildResolvConf(cmd, resolvConfPath); err != nil { return nil, nil, "", nil, "", err } @@ -169,7 +171,7 @@ func generateNetOpts(cmd *cobra.Command, dataStore, stateDir, ns, id string) ([] if err != nil { return nil, nil, "", nil, "", err } - opts = append(opts, withCustomResolvConf(resolvConfPath), withCustomHosts(etcHostsPath)) + opts = append(opts, WithCustomResolvConf(resolvConfPath), WithCustomHosts(etcHostsPath)) for _, p := range portSlice { pm, err := portutil.ParseFlagP(p) if err != nil { @@ -182,7 +184,7 @@ func generateNetOpts(cmd *cobra.Command, dataStore, stateDir, ns, id string) ([] if macAddress != "" { return nil, nil, "", nil, "", errors.New("conflicting options: mac-address and the network mode") } - if err := verifyContainerNetwork(cmd, netSlice); err != nil { + if err := VerifyContainerNetwork(cmd, netSlice); err != nil { return nil, nil, "", nil, "", err } network := strings.Split(netSlice[0], ":") @@ -190,7 +192,7 @@ func generateNetOpts(cmd *cobra.Command, dataStore, stateDir, ns, id string) ([] return nil, nil, "", nil, "", fmt.Errorf("invalid network: %s, should be \"container:\"", netSlice[0]) } containerName := network[1] - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return nil, nil, "", nil, "", err } @@ -204,7 +206,7 @@ func generateNetOpts(cmd *cobra.Command, dataStore, stateDir, ns, id string) ([] } containerID := found.Container.ID() - conStateDir, err := getContainerStateDirPath(cmd, dataStore, containerID) + conStateDir, err := common.ContainerStateDirPath(cmd, dataStore, containerID) if err != nil { return err } @@ -217,7 +219,7 @@ func generateNetOpts(cmd *cobra.Command, dataStore, stateDir, ns, id string) ([] hostnamePath := filepath.Join(conStateDir, "hostname") resolvConfPath := filepath.Join(conStateDir, "resolv.conf") etcHostsPath := hostsstore.HostsPath(dataStore, ns, containerID) - netNSPath, err := getContainerNetNSPath(ctx, found.Container) + netNSPath, err := ContainerNetNSPath(ctx, found.Container) if err != nil { return err } @@ -226,10 +228,10 @@ func generateNetOpts(cmd *cobra.Command, dataStore, stateDir, ns, id string) ([] Type: specs.NetworkNamespace, Path: netNSPath, }), - withCustomResolvConf(resolvConfPath), - withCustomHosts(etcHostsPath), + WithCustomResolvConf(resolvConfPath), + WithCustomHosts(etcHostsPath), oci.WithHostname(hostname), - withCustomEtcHostname(hostnamePath), + WithCustomEtcHostname(hostnamePath), ) // stored in labels with key "nerdctl/networks" netSlice = []string{fmt.Sprintf("container:%s", containerID)} @@ -249,7 +251,7 @@ func generateNetOpts(cmd *cobra.Command, dataStore, stateDir, ns, id string) ([] return opts, netSlice, ipAddress, ports, macAddress, nil } -func getContainerNetNSPath(ctx context.Context, c containerd.Container) (string, error) { +func ContainerNetNSPath(ctx context.Context, c containerd.Container) (string, error) { task, err := c.Task(ctx, nil) if err != nil { return "", err @@ -264,7 +266,7 @@ func getContainerNetNSPath(ctx context.Context, c containerd.Container) (string, return fmt.Sprintf("/proc/%d/ns/net", task.Pid()), nil } -func verifyCNINetwork(cmd *cobra.Command, netSlice []string, macAddress string) error { +func VerifyCNINetwork(cmd *cobra.Command, netSlice []string, macAddress string) error { cniPath, err := cmd.Flags().GetString("cni-path") if err != nil { return err @@ -297,7 +299,7 @@ func verifyCNINetwork(cmd *cobra.Command, netSlice []string, macAddress string) return nil } -func verifyContainerNetwork(cmd *cobra.Command, netSlice []string) error { +func VerifyContainerNetwork(cmd *cobra.Command, netSlice []string) error { if cmd.Flags().Changed("publish") { return fmt.Errorf("conflicting options: port publishing and the container type network mode") } @@ -319,7 +321,7 @@ func verifyContainerNetwork(cmd *cobra.Command, netSlice []string) error { return nil } -func buildResolvConf(cmd *cobra.Command, resolvConfPath string) error { +func BuildResolvConf(cmd *cobra.Command, resolvConfPath string) error { dnsValue, err := cmd.Flags().GetStringSlice("dns") if err != nil { return err @@ -385,7 +387,7 @@ func buildResolvConf(cmd *cobra.Command, resolvConfPath string) error { return nil } -func getMACAddress(cmd *cobra.Command, netSlice []string) (string, error) { +func MACAddress(cmd *cobra.Command, netSlice []string) (string, error) { macAddress, err := cmd.Flags().GetString("mac-address") if err != nil { return "", err diff --git a/cmd/nerdctl/run_restart.go b/cmd/nerdctl/utils/action/run/run_restart.go similarity index 93% rename from cmd/nerdctl/run_restart.go rename to cmd/nerdctl/utils/action/run/run_restart.go index 7c60b2c9bd4..7a5e4331946 100644 --- a/cmd/nerdctl/run_restart.go +++ b/cmd/nerdctl/utils/action/run/run_restart.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package run import ( "context" @@ -27,7 +27,7 @@ import ( "github.com/containerd/nerdctl/pkg/strutil" ) -func generateRestartOpts(ctx context.Context, client *containerd.Client, restartFlag, logURI string) ([]containerd.NewContainerOpts, error) { +func GenerateRestartOpts(ctx context.Context, client *containerd.Client, restartFlag, logURI string) ([]containerd.NewContainerOpts, error) { policySlice := strings.Split(restartFlag, ":") switch policySlice[0] { case "", "no": @@ -59,7 +59,7 @@ func generateRestartOpts(ctx context.Context, client *containerd.Client, restart return opts, nil } -func updateContainerStoppedLabel(ctx context.Context, container containerd.Container, stopped bool) error { +func UpdateContainerStoppedLabel(ctx context.Context, container containerd.Container, stopped bool) error { opt := containerd.WithAdditionalContainerLabels(map[string]string{ restart.ExplicitlyStoppedLabel: strconv.FormatBool(stopped), }) diff --git a/cmd/nerdctl/run_runtime.go b/cmd/nerdctl/utils/action/run/run_runtime.go similarity index 96% rename from cmd/nerdctl/run_runtime.go rename to cmd/nerdctl/utils/action/run/run_runtime.go index c10f01b5bc8..0c92bdd983f 100644 --- a/cmd/nerdctl/run_runtime.go +++ b/cmd/nerdctl/utils/action/run/run_runtime.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package run import ( "context" @@ -30,7 +30,7 @@ import ( "github.com/spf13/cobra" ) -func generateRuntimeCOpts(cmd *cobra.Command) ([]containerd.NewContainerOpts, error) { +func GenerateRuntimeCOpts(cmd *cobra.Command) ([]containerd.NewContainerOpts, error) { runtime := plugin.RuntimeRuncV2 var ( runcOpts runcoptions.Options diff --git a/cmd/nerdctl/run_security_linux.go b/cmd/nerdctl/utils/action/run/run_security_linux.go similarity index 99% rename from cmd/nerdctl/run_security_linux.go rename to cmd/nerdctl/utils/action/run/run_security_linux.go index 7d89f682193..f160c14b883 100644 --- a/cmd/nerdctl/run_security_linux.go +++ b/cmd/nerdctl/utils/action/run/run_security_linux.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package run import ( "errors" diff --git a/cmd/nerdctl/run_ulimit.go b/cmd/nerdctl/utils/action/run/run_ulimit.go similarity index 99% rename from cmd/nerdctl/run_ulimit.go rename to cmd/nerdctl/utils/action/run/run_ulimit.go index 41d86db82c0..cb0d0da0e44 100644 --- a/cmd/nerdctl/run_ulimit.go +++ b/cmd/nerdctl/utils/action/run/run_ulimit.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package run import ( "context" diff --git a/cmd/nerdctl/run_user.go b/cmd/nerdctl/utils/action/run/run_user.go similarity index 85% rename from cmd/nerdctl/run_user.go rename to cmd/nerdctl/utils/action/run/run_user.go index 5ecffc67a22..0fc977de8da 100644 --- a/cmd/nerdctl/run_user.go +++ b/cmd/nerdctl/utils/action/run/run_user.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package run import ( "context" @@ -26,35 +26,26 @@ import ( "github.com/spf13/cobra" ) -func generateUserOpts(cmd *cobra.Command) ([]oci.SpecOpts, error) { +func GenerateUserOpts(cmd *cobra.Command) ([]oci.SpecOpts, error) { var opts []oci.SpecOpts user, err := cmd.Flags().GetString("user") if err != nil { return nil, err } if user != "" { - opts = append(opts, oci.WithUser(user), withResetAdditionalGIDs(), oci.WithAdditionalGIDs(user)) + opts = append(opts, oci.WithUser(user), WithResetAdditionalGIDs(), oci.WithAdditionalGIDs(user)) } return opts, nil } -func generateUmaskOpts(cmd *cobra.Command) ([]oci.SpecOpts, error) { - var opts []oci.SpecOpts - umask, err := cmd.Flags().GetString("umask") - if err != nil { - return nil, err - } - if cmd.Flags().Changed("umask") && umask != "" { - decVal, err := strconv.ParseUint(umask, 8, 32) - if err != nil { - return nil, fmt.Errorf("invalid Umask Value:%s", umask) - } - opts = append(opts, withAdditionalUmask(uint32(decVal))) +func WithResetAdditionalGIDs() oci.SpecOpts { + return func(_ context.Context, _ oci.Client, _ *containers.Container, s *oci.Spec) error { + s.Process.User.AdditionalGids = nil + return nil } - return opts, nil } -func generateGroupsOpts(cmd *cobra.Command) ([]oci.SpecOpts, error) { +func GenerateGroupsOpts(cmd *cobra.Command) ([]oci.SpecOpts, error) { var opts []oci.SpecOpts groups, err := cmd.Flags().GetStringSlice("group-add") if err != nil { @@ -66,11 +57,20 @@ func generateGroupsOpts(cmd *cobra.Command) ([]oci.SpecOpts, error) { return opts, nil } -func withResetAdditionalGIDs() oci.SpecOpts { - return func(_ context.Context, _ oci.Client, _ *containers.Container, s *oci.Spec) error { - s.Process.User.AdditionalGids = nil - return nil +func GenerateUmaskOpts(cmd *cobra.Command) ([]oci.SpecOpts, error) { + var opts []oci.SpecOpts + umask, err := cmd.Flags().GetString("umask") + if err != nil { + return nil, err } + if cmd.Flags().Changed("umask") && umask != "" { + decVal, err := strconv.ParseUint(umask, 8, 32) + if err != nil { + return nil, fmt.Errorf("invalid Umask Value:%s", umask) + } + opts = append(opts, withAdditionalUmask(uint32(decVal))) + } + return opts, nil } func withAdditionalUmask(umask uint32) oci.SpecOpts { diff --git a/cmd/nerdctl/run_windows.go b/cmd/nerdctl/utils/action/run/run_windows.go similarity index 77% rename from cmd/nerdctl/run_windows.go rename to cmd/nerdctl/utils/action/run/run_windows.go index 813570b37d8..05de52b1859 100644 --- a/cmd/nerdctl/run_windows.go +++ b/cmd/nerdctl/utils/action/run/run_windows.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package run import ( "context" @@ -23,25 +23,12 @@ import ( "github.com/containerd/containerd" "github.com/containerd/containerd/containers" "github.com/containerd/containerd/oci" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/common" "github.com/docker/go-units" "github.com/spf13/cobra" ) -func WithoutRunMount() func(ctx context.Context, client oci.Client, c *containers.Container, s *oci.Spec) error { - // not valid on windows - return func(_ context.Context, _ oci.Client, _ *containers.Container, s *oci.Spec) error { return nil } -} - -func capShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - candidates := []string{} - return candidates, cobra.ShellCompDirectiveNoFileComp -} - -func runShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return nil, cobra.ShellCompDirectiveNoFileComp -} - -func setPlatformOptions(ctx context.Context, opts []oci.SpecOpts, cmd *cobra.Command, client *containerd.Client, id string, internalLabels internalLabels) ([]oci.SpecOpts, internalLabels, error) { +func SetPlatformOptions(ctx context.Context, opts []oci.SpecOpts, cmd *cobra.Command, client *containerd.Client, id string, internalLabels common.InternalLabels) ([]oci.SpecOpts, common.InternalLabels, error) { cpus, err := cmd.Flags().GetFloat64("cpus") if err != nil { return nil, internalLabels, err @@ -68,3 +55,8 @@ func setPlatformOptions(ctx context.Context, opts []oci.SpecOpts, cmd *cobra.Com return opts, internalLabels, nil } + +func WithoutRunMount() func(ctx context.Context, client oci.Client, c *containers.Container, s *oci.Spec) error { + // not valid on windows + return func(_ context.Context, _ oci.Client, _ *containers.Container, s *oci.Spec) error { return nil } +} diff --git a/cmd/nerdctl/start.go b/cmd/nerdctl/utils/action/start.go similarity index 65% rename from cmd/nerdctl/start.go rename to cmd/nerdctl/utils/action/start.go index 147ca221d31..6b3b980e0d1 100644 --- a/cmd/nerdctl/start.go +++ b/cmd/nerdctl/utils/action/start.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package action import ( "context" @@ -30,79 +30,16 @@ import ( "github.com/containerd/containerd/cio" "github.com/containerd/containerd/cmd/ctr/commands" "github.com/containerd/containerd/oci" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/action/run" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/common" "github.com/containerd/nerdctl/pkg/formatter" - "github.com/containerd/nerdctl/pkg/idutil/containerwalker" "github.com/containerd/nerdctl/pkg/labels" "github.com/containerd/nerdctl/pkg/netutil/nettype" "github.com/opencontainers/runtime-spec/specs-go" - "github.com/sirupsen/logrus" - "github.com/spf13/cobra" ) -func newStartCommand() *cobra.Command { - var startCommand = &cobra.Command{ - Use: "start [flags] CONTAINER [CONTAINER, ...]", - Args: cobra.MinimumNArgs(1), - Short: "Start one or more running containers", - RunE: startAction, - ValidArgsFunction: startShellComplete, - SilenceUsage: true, - SilenceErrors: true, - } - - startCommand.Flags().SetInterspersed(false) - startCommand.Flags().BoolP("attach", "a", false, "Attach STDOUT/STDERR and forward signals") - - return startCommand -} - -func startAction(cmd *cobra.Command, args []string) error { - client, ctx, cancel, err := newClient(cmd) - if err != nil { - return err - } - defer cancel() - - flagA, err := cmd.Flags().GetBool("attach") - if err != nil { - return err - } - - if flagA && len(args) > 1 { - return fmt.Errorf("you cannot start and attach multiple containers at once") - } - - walker := &containerwalker.ContainerWalker{ - Client: client, - OnFound: func(ctx context.Context, found containerwalker.Found) error { - if found.MatchCount > 1 { - return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req) - } - if err := startContainer(ctx, found.Container, flagA, client); err != nil { - return err - } - if !flagA { - _, err := fmt.Fprintf(cmd.OutOrStdout(), "%s\n", found.Req) - if err != nil { - return err - } - } - return err - }, - } - for _, req := range args { - n, err := walker.Walk(ctx, req) - if err != nil { - return err - } else if n == 0 { - return fmt.Errorf("no such container %s", req) - } - } - return nil -} - -func startContainer(ctx context.Context, container containerd.Container, flagA bool, client *containerd.Client) error { +func StartContainer(ctx context.Context, container containerd.Container, flagA bool, client *containerd.Client) error { lab, err := container.Labels(ctx) if err != nil { return err @@ -139,7 +76,7 @@ func startContainer(ctx context.Context, container containerd.Container, flagA b logrus.Warnf("container %s is already running", container.ID()) return nil } - if err := updateContainerStoppedLabel(ctx, container, false); err != nil { + if err := run.UpdateContainerStoppedLabel(ctx, container, false); err != nil { return err } if oldTask, err := container.Task(ctx, nil); err == nil { @@ -176,8 +113,8 @@ func startContainer(ctx context.Context, container containerd.Container, flagA b return err } if code != 0 { - return ExitCodeError{ - exitCode: int(code), + return common.ExitCodeError{ + Code: int(code), } } return nil @@ -205,7 +142,7 @@ func reconfigNetContainer(ctx context.Context, c containerd.Container, client *c if err != nil { return err } - netNSPath, err := getContainerNetNSPath(ctx, targetCon) + netNSPath, err := run.ContainerNetNSPath(ctx, targetCon) if err != nil { return err } @@ -242,7 +179,7 @@ func reconfigPIDContainer(ctx context.Context, c containerd.Container, client *c return err } - opts, err := generateSharingPIDOpts(ctx, targetCon) + opts, err := run.GenerateSharingPIDOpts(ctx, targetCon) if err != nil { return err } @@ -261,11 +198,3 @@ func reconfigPIDContainer(ctx context.Context, c containerd.Container, client *c return nil } - -func startShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - // show non-running container names - statusFilterFn := func(st containerd.ProcessStatus) bool { - return st != containerd.Running && st != containerd.Unknown - } - return shellCompleteContainerNames(cmd, statusFilterFn) -} diff --git a/cmd/nerdctl/stop.go b/cmd/nerdctl/utils/action/stop.go similarity index 57% rename from cmd/nerdctl/stop.go rename to cmd/nerdctl/utils/action/stop.go index 21227bef1fc..157938dd0a9 100644 --- a/cmd/nerdctl/stop.go +++ b/cmd/nerdctl/utils/action/stop.go @@ -14,87 +14,23 @@ limitations under the License. */ -package main +package action import ( "context" "fmt" "time" - "github.com/spf13/cobra" - "github.com/containerd/containerd" "github.com/containerd/containerd/cio" - "github.com/containerd/containerd/errdefs" - "github.com/containerd/nerdctl/pkg/idutil/containerwalker" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/action/run" "github.com/containerd/nerdctl/pkg/labels" - "github.com/moby/sys/signal" "github.com/sirupsen/logrus" ) -func newStopCommand() *cobra.Command { - var stopCommand = &cobra.Command{ - Use: "stop [flags] CONTAINER [CONTAINER, ...]", - Args: cobra.MinimumNArgs(1), - Short: "Stop one or more running containers", - RunE: stopAction, - ValidArgsFunction: stopShellComplete, - SilenceUsage: true, - SilenceErrors: true, - } - stopCommand.Flags().IntP("time", "t", 10, "Seconds to wait for stop before killing it") - return stopCommand -} - -func stopAction(cmd *cobra.Command, args []string) error { - // Time to wait after sending a SIGTERM and before sending a SIGKILL. - var timeout *time.Duration - if cmd.Flags().Changed("time") { - timeValue, err := cmd.Flags().GetInt("time") - if err != nil { - return err - } - t := time.Duration(timeValue) * time.Second - timeout = &t - } - - client, ctx, cancel, err := newClient(cmd) - if err != nil { - return err - } - defer cancel() - - walker := &containerwalker.ContainerWalker{ - Client: client, - OnFound: func(ctx context.Context, found containerwalker.Found) error { - if found.MatchCount > 1 { - return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req) - } - if err := stopContainer(ctx, found.Container, timeout); err != nil { - if errdefs.IsNotFound(err) { - fmt.Fprintf(cmd.ErrOrStderr(), "No such container: %s\n", found.Req) - return nil - } - return err - } - _, err := fmt.Fprintf(cmd.OutOrStdout(), "%s\n", found.Req) - return err - }, - } - for _, req := range args { - n, err := walker.Walk(ctx, req) - if err != nil { - return err - } else if n == 0 { - return fmt.Errorf("no such container %s", req) - } - } - return nil -} - -func stopContainer(ctx context.Context, container containerd.Container, timeout *time.Duration) error { - if err := updateContainerStoppedLabel(ctx, container, true); err != nil { +func StopContainer(ctx context.Context, container containerd.Container, timeout *time.Duration) error { + if err := run.UpdateContainerStoppedLabel(ctx, container, true); err != nil { return err } @@ -210,11 +146,3 @@ func waitContainerStop(ctx context.Context, exitCh <-chan containerd.ExitStatus, return status.Error() } } - -func stopShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - // show non-stopped container names - statusFilterFn := func(st containerd.ProcessStatus) bool { - return st != containerd.Stopped && st != containerd.Created && st != containerd.Unknown - } - return shellCompleteContainerNames(cmd, statusFilterFn) -} diff --git a/cmd/nerdctl/top.go b/cmd/nerdctl/utils/action/top.go similarity index 75% rename from cmd/nerdctl/top.go rename to cmd/nerdctl/utils/action/top.go index 4f9bf852b9d..7462c1c1a9a 100644 --- a/cmd/nerdctl/top.go +++ b/cmd/nerdctl/utils/action/top.go @@ -23,7 +23,7 @@ NOTICE: https://github.com/moby/moby/blob/v20.10.6/NOTICE */ -package main +package action import ( "bytes" @@ -37,10 +37,6 @@ import ( "text/tabwriter" "github.com/containerd/containerd" - "github.com/containerd/nerdctl/pkg/idutil/containerwalker" - "github.com/containerd/nerdctl/pkg/infoutil" - "github.com/containerd/nerdctl/pkg/rootlessutil" - "github.com/spf13/cobra" ) @@ -60,63 +56,6 @@ type ContainerTopOKBody struct { Titles []string `json:"Titles"` } -func newTopCommand() *cobra.Command { - var topCommand = &cobra.Command{ - Use: "top CONTAINER [ps OPTIONS]", - Args: cobra.MinimumNArgs(1), - Short: "Display the running processes of a container", - RunE: topAction, - ValidArgsFunction: topShellComplete, - SilenceUsage: true, - SilenceErrors: true, - } - topCommand.Flags().SetInterspersed(false) - return topCommand -} - -func topAction(cmd *cobra.Command, args []string) error { - // NOTE: rootless container does not rely on cgroupv1. - // more details about possible ways to resolve this concern: #223 - if rootlessutil.IsRootless() && infoutil.CgroupsVersion() == "1" { - return fmt.Errorf("top requires cgroup v2 for rootless containers, see https://rootlesscontaine.rs/getting-started/common/cgroup2/") - } - - cgroupManager, err := cmd.Flags().GetString("cgroup-manager") - if err != nil { - return err - } - if cgroupManager == "none" { - return errors.New("cgroup manager must not be \"none\"") - } - - client, ctx, cancel, err := newClient(cmd) - if err != nil { - return err - } - defer cancel() - - walker := &containerwalker.ContainerWalker{ - Client: client, - OnFound: func(ctx context.Context, found containerwalker.Found) error { - if found.MatchCount > 1 { - return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req) - } - if err := containerTop(ctx, cmd, client, found.Container.ID(), strings.Join(args[1:], " ")); err != nil { - return err - } - return nil - }, - } - - n, err := walker.Walk(ctx, args[0]) - if err != nil { - return err - } else if n == 0 { - return fmt.Errorf("no such container %s", args[0]) - } - return nil -} - // appendProcess2ProcList is from https://github.com/moby/moby/blob/v20.10.6/daemon/top_unix.go#L49-L55 func appendProcess2ProcList(procList *ContainerTopOKBody, fields []string) { // Make sure number of fields equals number of header titles @@ -239,14 +178,14 @@ func parsePSOutput(output []byte, procs []uint32) (*ContainerTopOKBody, error) { return procList, nil } -// containerTop was inspired from https://github.com/moby/moby/blob/v20.10.6/daemon/top_unix.go#L133-L189 +// ContainerTop was inspired from https://github.com/moby/moby/blob/v20.10.6/daemon/top_unix.go#L133-L189 // // ContainerTop lists the processes running inside of the given // container by calling ps with the given args, or with the flags // "-ef" if no args are given. An error is returned if the container // is not found, or is not running, or if there are any problems // running ps, or parsing the output. -func containerTop(ctx context.Context, cmd *cobra.Command, client *containerd.Client, id string, psArgs string) error { +func ContainerTop(ctx context.Context, cmd *cobra.Command, client *containerd.Client, id string, psArgs string) error { if psArgs == "" { psArgs = "-ef" } @@ -318,11 +257,3 @@ func containerTop(ctx context.Context, cmd *cobra.Command, client *containerd.Cl return w.Flush() } - -func topShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - // show running container names - statusFilterFn := func(st containerd.ProcessStatus) bool { - return st == containerd.Running - } - return shellCompleteContainerNames(cmd, statusFilterFn) -} diff --git a/cmd/nerdctl/utils/action/unpause.go b/cmd/nerdctl/utils/action/unpause.go new file mode 100644 index 00000000000..bfeaaf8fc47 --- /dev/null +++ b/cmd/nerdctl/utils/action/unpause.go @@ -0,0 +1,49 @@ +/* + 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 action + +import ( + "context" + "fmt" + + "github.com/containerd/containerd" + "github.com/containerd/containerd/cio" +) + +func UnpauseContainer(ctx context.Context, client *containerd.Client, id string) error { + container, err := client.LoadContainer(ctx, id) + if err != nil { + return err + } + + task, err := container.Task(ctx, cio.Load) + if err != nil { + return err + } + + status, err := task.Status(ctx) + if err != nil { + return err + } + + switch status.Status { + case containerd.Paused: + return task.Resume(ctx) + default: + return fmt.Errorf("container %s is not paused", id) + } +} diff --git a/cmd/nerdctl/utils/command.go b/cmd/nerdctl/utils/command.go new file mode 100644 index 00000000000..1ae62decfe6 --- /dev/null +++ b/cmd/nerdctl/utils/command.go @@ -0,0 +1,224 @@ +/* + 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 utils + +import ( + "fmt" + "os" + "strconv" + + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +// AddStringFlag is similar to cmd.Flags().String but supports aliases and env var +func AddStringFlag(cmd *cobra.Command, name string, aliases []string, value string, env, usage string) { + if env != "" { + usage = fmt.Sprintf("%s [$%s]", usage, env) + } + if envV, ok := os.LookupEnv(env); ok { + value = envV + } + aliasesUsage := fmt.Sprintf("Alias of --%s", name) + p := new(string) + flags := cmd.Flags() + flags.StringVar(p, name, value, usage) + for _, a := range aliases { + if len(a) == 1 { + // pflag doesn't support short-only flags, so we have to register long one as well here + flags.StringVarP(p, a, a, value, aliasesUsage) + } else { + flags.StringVar(p, a, value, aliasesUsage) + } + } +} + +// AddPersistentStringFlag is similar to AddStringFlag but persistent. +// See https://github.com/spf13/cobra/blob/main/user_guide.md#persistent-flags to learn what is "persistent". +func AddPersistentStringFlag(cmd *cobra.Command, name string, aliases, localAliases, persistentAliases []string, aliasToBeInherited *pflag.FlagSet, value string, env, usage string) { + if env != "" { + usage = fmt.Sprintf("%s [$%s]", usage, env) + } + if envV, ok := os.LookupEnv(env); ok { + value = envV + } + aliasesUsage := fmt.Sprintf("Alias of --%s", name) + p := new(string) + + // flags is full set of flag(s) + // flags can redefine alias already used in subcommands + flags := cmd.Flags() + for _, a := range aliases { + if len(a) == 1 { + // pflag doesn't support short-only flags, so we have to register long one as well here + flags.StringVarP(p, a, a, value, aliasesUsage) + } else { + flags.StringVar(p, a, value, aliasesUsage) + } + // non-persistent flags are not added to the InheritedFlags, so we should add them manually + f := flags.Lookup(a) + aliasToBeInherited.AddFlag(f) + } + + // localFlags are local to the rootCmd + localFlags := cmd.LocalFlags() + for _, a := range localAliases { + if len(a) == 1 { + // pflag doesn't support short-only flags, so we have to register long one as well here + localFlags.StringVarP(p, a, a, value, aliasesUsage) + } else { + localFlags.StringVar(p, a, value, aliasesUsage) + } + } + + // persistentFlags cannot redefine alias already used in subcommands + persistentFlags := cmd.PersistentFlags() + persistentFlags.StringVar(p, name, value, usage) + for _, a := range persistentAliases { + if len(a) == 1 { + // pflag doesn't support short-only flags, so we have to register long one as well here + persistentFlags.StringVarP(p, a, a, value, aliasesUsage) + } else { + persistentFlags.StringVar(p, a, value, aliasesUsage) + } + } +} + +// AddPersistentBoolFlag is similar to AddBoolFlag but persistent. +// See https://github.com/spf13/cobra/blob/main/user_guide.md#persistent-flags to learn what is "persistent". +func AddPersistentBoolFlag(cmd *cobra.Command, name string, aliases, nonPersistentAliases []string, value bool, env, usage string) { + if env != "" { + usage = fmt.Sprintf("%s [$%s]", usage, env) + } + if envV, ok := os.LookupEnv(env); ok { + var err error + value, err = strconv.ParseBool(envV) + if err != nil { + logrus.WithError(err).Warnf("Invalid boolean value for `%s`", env) + } + } + aliasesUsage := fmt.Sprintf("Alias of --%s", name) + p := new(bool) + flags := cmd.Flags() + for _, a := range nonPersistentAliases { + if len(a) == 1 { + // pflag doesn't support short-only flags, so we have to register long one as well here + flags.BoolVarP(p, a, a, value, aliasesUsage) + } else { + flags.BoolVar(p, a, value, aliasesUsage) + } + } + + persistentFlags := cmd.PersistentFlags() + persistentFlags.BoolVar(p, name, value, usage) + for _, a := range aliases { + if len(a) == 1 { + // pflag doesn't support short-only flags, so we have to register long one as well here + persistentFlags.BoolVarP(p, a, a, value, aliasesUsage) + } else { + persistentFlags.BoolVar(p, a, value, aliasesUsage) + } + } +} + +// AddPersistentStringArrayFlag is similar to cmd.Flags().StringArray but supports aliases and env var and persistent. +// See https://github.com/spf13/cobra/blob/main/user_guide.md#persistent-flags to learn what is "persistent". +func AddPersistentStringArrayFlag(cmd *cobra.Command, name string, aliases, nonPersistentAliases []string, value []string, env string, usage string) { + if env != "" { + usage = fmt.Sprintf("%s [$%s]", usage, env) + } + if envV, ok := os.LookupEnv(env); ok { + value = []string{envV} + } + aliasesUsage := fmt.Sprintf("Alias of --%s", name) + p := new([]string) + flags := cmd.Flags() + for _, a := range nonPersistentAliases { + if len(a) == 1 { + // pflag doesn't support short-only flags, so we have to register long one as well here + flags.StringArrayVarP(p, a, a, value, aliasesUsage) + } else { + flags.StringArrayVar(p, a, value, aliasesUsage) + } + } + + persistentFlags := cmd.PersistentFlags() + persistentFlags.StringArrayVar(p, name, value, usage) + for _, a := range aliases { + if len(a) == 1 { + // pflag doesn't support short-only flags, so we have to register long one as well here + persistentFlags.StringArrayVarP(p, a, a, value, aliasesUsage) + } else { + persistentFlags.StringArrayVar(p, a, value, aliasesUsage) + } + } +} + +func CheckExperimental(feature string) func(cmd *cobra.Command, args []string) error { + return func(cmd *cobra.Command, args []string) error { + experimental, err := cmd.Flags().GetBool("experimental") + if err != nil { + return err + } + if !experimental { + return fmt.Errorf("%s is experimental feature, you should enable experimental config", feature) + } + return nil + } +} + +// IsExactArgs returns an error if there is not the exact number of args +func IsExactArgs(number int) cobra.PositionalArgs { + return func(cmd *cobra.Command, args []string) error { + if len(args) == number { + return nil + } + return fmt.Errorf( + "%q requires exactly %d %s.\nSee '%s --help'.\n\nUsage: %s\n\n%s", + cmd.CommandPath(), + number, + "argument(s)", + cmd.CommandPath(), + cmd.UseLine(), + cmd.Short, + ) + } +} + +func GlobalFlags(cmd *cobra.Command) (string, []string) { + args0, err := os.Executable() + if err != nil { + logrus.WithError(err).Warnf("cannot call os.Executable(), assuming the executable to be %q", os.Args[0]) + args0 = os.Args[0] + } + if len(os.Args) < 2 { + return args0, nil + } + + rootCmd := cmd.Root() + flagSet := rootCmd.Flags() + args := []string{} + flagSet.VisitAll(func(f *pflag.Flag) { + key := f.Name + val := f.Value.String() + if f.Changed { + args = append(args, "--"+key+"="+val) + } + }) + return args0, args +} diff --git a/cmd/nerdctl/utils/common/common.go b/cmd/nerdctl/utils/common/common.go new file mode 100644 index 00000000000..f9e1814e47d --- /dev/null +++ b/cmd/nerdctl/utils/common/common.go @@ -0,0 +1,278 @@ +/* + 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 common + +import ( + "context" + "encoding/json" + "errors" + "io" + "os" + "path/filepath" + "strings" + + "github.com/containerd/containerd" + "github.com/containerd/containerd/snapshots" + "github.com/containerd/go-cni" + "github.com/containerd/nerdctl/pkg/inspecttypes/dockercompat" + "github.com/containerd/nerdctl/pkg/labels" + "github.com/containerd/nerdctl/pkg/mountutil" + "github.com/containerd/nerdctl/pkg/platformutil" + "github.com/containerd/nerdctl/pkg/strutil" + "github.com/docker/cli/opts" + "github.com/spf13/cobra" +) + +const ( + Category = "category" + Management = "management" + TiniInitBinary = "tini" +) + +type ReadCounter struct { + io.Reader + N int +} + +func (r *ReadCounter) Read(p []byte) (int, error) { + n, err := r.Reader.Read(p) + if n > 0 { + r.N += n + } + return n, err +} + +type SnapshotKey string + +type DockerArchiveManifestJSONEntry struct { + Config string + RepoTags []string + Layers []string +} + +type DockerArchiveManifestJSON []DockerArchiveManifestJSONEntry + +// recursive function to calculate total usage of key's parent +func (key SnapshotKey) Add(ctx context.Context, s snapshots.Snapshotter, usage *snapshots.Usage) error { + if key == "" { + return nil + } + u, err := s.Usage(ctx, string(key)) + if err != nil { + return err + } + + usage.Add(u) + + info, err := s.Stat(ctx, string(key)) + if err != nil { + return err + } + + key = SnapshotKey(info.Parent) + return key.Add(ctx, s, usage) +} + +// removing a non-stoped/non-created container without force, will cause a error +type StatusError struct { + error +} + +func NewStatusError(err error) StatusError { + return StatusError{err} +} + +func (e StatusError) Error() string { + return e.error.Error() +} + +type ExitCoder interface { + error + ExitCode() int +} + +type ExitCodeError struct { + error + Code int +} + +func (e ExitCodeError) ExitCode() int { + return e.Code +} + +func HandleExitCoder(err error) { + if err == nil { + return + } + + if exitErr, ok := err.(ExitCoder); ok { + os.Exit(exitErr.ExitCode()) + } +} + +type InternalLabels struct { + // labels from cmd options + Namespace string + Platform string + ExtraHosts []string + PidFile string + // labels from cmd options or automatically set + Name string + Hostname string + // automatically generated + StateDir string + // network + Networks []string + IPAddress string + Ports []cni.PortMapping + MacAddress string + // volumn + MountPoints []*mountutil.Processed + AnonVolumes []string + // pid Namespace + PidContainer string + // log + LogURI string +} + +func WithInternalLabels(internalLabels InternalLabels) (containerd.NewContainerOpts, error) { + m := make(map[string]string) + m[labels.Namespace] = internalLabels.Namespace + if internalLabels.Name != "" { + m[labels.Name] = internalLabels.Name + } + m[labels.Hostname] = internalLabels.Hostname + extraHostsJSON, err := json.Marshal(internalLabels.ExtraHosts) + if err != nil { + return nil, err + } + m[labels.ExtraHosts] = string(extraHostsJSON) + m[labels.StateDir] = internalLabels.StateDir + networksJSON, err := json.Marshal(internalLabels.Networks) + if err != nil { + return nil, err + } + m[labels.Networks] = string(networksJSON) + if len(internalLabels.Ports) > 0 { + portsJSON, err := json.Marshal(internalLabels.Ports) + if err != nil { + return nil, err + } + m[labels.Ports] = string(portsJSON) + } + if internalLabels.LogURI != "" { + m[labels.LogURI] = internalLabels.LogURI + } + if len(internalLabels.AnonVolumes) > 0 { + anonVolumeJSON, err := json.Marshal(internalLabels.AnonVolumes) + if err != nil { + return nil, err + } + m[labels.AnonymousVolumes] = string(anonVolumeJSON) + } + + if internalLabels.PidFile != "" { + m[labels.PIDFile] = internalLabels.PidFile + } + + if internalLabels.IPAddress != "" { + m[labels.IPAddress] = internalLabels.IPAddress + } + + m[labels.Platform], err = platformutil.NormalizeString(internalLabels.Platform) + if err != nil { + return nil, err + } + + if len(internalLabels.MountPoints) > 0 { + mounts := DockercompatMounts(internalLabels.MountPoints) + mountPointsJSON, err := json.Marshal(mounts) + if err != nil { + return nil, err + } + m[labels.Mounts] = string(mountPointsJSON) + } + + if internalLabels.MacAddress != "" { + m[labels.MACAddress] = internalLabels.MacAddress + } + + if internalLabels.PidContainer != "" { + m[labels.PIDContainer] = internalLabels.PidContainer + } + + return containerd.WithAdditionalContainerLabels(m), nil +} + +func DockercompatMounts(mountPoints []*mountutil.Processed) []dockercompat.MountPoint { + reuslt := make([]dockercompat.MountPoint, len(mountPoints)) + for i := range mountPoints { + mp := mountPoints[i] + reuslt[i] = dockercompat.MountPoint{ + Type: mp.Type, + Name: mp.Name, + Source: mp.Mount.Source, + Destination: mp.Mount.Destination, + Driver: "", + Mode: mp.Mode, + } + + // it's a anonymous volume + if mp.AnonymousVolume != "" { + reuslt[i].Name = mp.AnonymousVolume + } + + // volume only support local driver + if mp.Type == "volume" { + reuslt[i].Driver = "local" + } + } + return reuslt +} + +func ReadKVStringsMapfFromLabel(cmd *cobra.Command) (map[string]string, error) { + labelsMap, err := cmd.Flags().GetStringArray("label") + if err != nil { + return nil, err + } + labelsMap = strutil.DedupeStrSlice(labelsMap) + labelsFilePath, err := cmd.Flags().GetStringSlice("label-file") + if err != nil { + return nil, err + } + labelsFilePath = strutil.DedupeStrSlice(labelsFilePath) + labels, err := opts.ReadKVStrings(labelsFilePath, labelsMap) + if err != nil { + return nil, err + } + + return strutil.ConvertKVStringsToMap(labels), nil +} + +func ContainerStateDirPath(cmd *cobra.Command, dataStore, id string) (string, error) { + ns, err := cmd.Flags().GetString("namespace") + if err != nil { + return "", err + } + if ns == "" { + return "", errors.New("namespace is required") + } + if strings.Contains(ns, "/") { + return "", errors.New("namespace with '/' is unsupported") + } + return filepath.Join(dataStore, "containers", ns, id), nil +} diff --git a/cmd/nerdctl/container_lsutil.go b/cmd/nerdctl/utils/containerfilter/containerfilter.go similarity index 52% rename from cmd/nerdctl/container_lsutil.go rename to cmd/nerdctl/utils/containerfilter/containerfilter.go index 314e940d9e6..1dcff0e4bac 100644 --- a/cmd/nerdctl/container_lsutil.go +++ b/cmd/nerdctl/utils/containerfilter/containerfilter.go @@ -14,10 +14,11 @@ limitations under the License. */ -package main +package containerfilter import ( "context" + "encoding/json" "fmt" "strconv" "strings" @@ -25,32 +26,35 @@ import ( "github.com/containerd/containerd" "github.com/containerd/containerd/containers" + "github.com/containerd/nerdctl/cmd/nerdctl/utils" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/volume" + "github.com/containerd/nerdctl/pkg/labels" "github.com/sirupsen/logrus" ) -func foldContainerFilters(ctx context.Context, containers []containerd.Container, filters []string) (*containerFilterContext, error) { - filterCtx := &containerFilterContext{containers: containers} +func FoldContainerFilters(ctx context.Context, containers []containerd.Container, filters []string) (*FilterContext, error) { + filterCtx := &FilterContext{Containers: containers} err := filterCtx.foldFilters(ctx, filters) return filterCtx, err } -type containerFilterContext struct { - containers []containerd.Container - - idFilterFuncs []func(string) bool - nameFilterFuncs []func(string) bool - exitedFilterFuncs []func(int) bool - beforeFilterFuncs []func(t time.Time) bool - sinceFilterFuncs []func(t time.Time) bool - statusFilterFuncs []func(containerd.ProcessStatus) bool - labelFilterFuncs []func(map[string]string) bool - volumeFilterFuncs []func([]*containerVolume) bool - networkFilterFuncs []func([]string) bool +type FilterContext struct { + Containers []containerd.Container + + IDFilterFuncs []func(string) bool + NameFilterFuncs []func(string) bool + ExitedFilterFuncs []func(int) bool + BeforeFilterFuncs []func(t time.Time) bool + SinceFilterFuncs []func(t time.Time) bool + StatusFilterFuncs []func(containerd.ProcessStatus) bool + LabelFilterFuncs []func(map[string]string) bool + VolumeFilterFuncs []func([]*volume.ContainerVolume) bool + NetworkFilterFuncs []func([]string) bool } -func (cl *containerFilterContext) MatchesFilters(ctx context.Context) []containerd.Container { - matchesContainers := make([]containerd.Container, 0, len(cl.containers)) - for _, container := range cl.containers { +func (cl *FilterContext) MatchesFilters(ctx context.Context) []containerd.Container { + matchesContainers := make([]containerd.Container, 0, len(cl.Containers)) + for _, container := range cl.Containers { if !cl.matchesInfoFilters(ctx, container) { continue } @@ -59,11 +63,11 @@ func (cl *containerFilterContext) MatchesFilters(ctx context.Context) []containe } matchesContainers = append(matchesContainers, container) } - cl.containers = matchesContainers - return cl.containers + cl.Containers = matchesContainers + return cl.Containers } -func (cl *containerFilterContext) foldFilters(ctx context.Context, filters []string) error { +func (cl *FilterContext) foldFilters(ctx context.Context, filters []string) error { folders := []struct { filterType string foldFunc func(context.Context, string, string) error @@ -82,7 +86,7 @@ func (cl *containerFilterContext) foldFilters(ctx context.Context, filters []str } splited := strings.SplitN(filter, "=", 2) if len(splited) != 2 { - return fmt.Errorf("invalid argument \"%s\" for \"-f, --filter\": bad format of filter (expected name=value)", folder.filterType) + return fmt.Errorf("invalid argument \"%s\" for \"-f, --filter\": bad format of filter (expected Name=value)", folder.filterType) } if err := folder.foldFunc(ctx, filter, splited[1]); err != nil { return err @@ -97,26 +101,26 @@ func (cl *containerFilterContext) foldFilters(ctx context.Context, filters []str return nil } -func (cl *containerFilterContext) foldExitedFilter(_ context.Context, filter, value string) error { +func (cl *FilterContext) foldExitedFilter(_ context.Context, filter, value string) error { exited, err := strconv.Atoi(value) if err != nil { return err } - cl.exitedFilterFuncs = append(cl.exitedFilterFuncs, func(exitStatus int) bool { + cl.ExitedFilterFuncs = append(cl.ExitedFilterFuncs, func(exitStatus int) bool { return exited == exitStatus }) return nil } -func (cl *containerFilterContext) foldStatusFilter(_ context.Context, filter, value string) error { +func (cl *FilterContext) foldStatusFilter(_ context.Context, filter, value string) error { status := containerd.ProcessStatus(value) switch status { case containerd.Running, containerd.Created, containerd.Stopped, containerd.Paused, containerd.Pausing, containerd.Unknown: - cl.statusFilterFuncs = append(cl.statusFilterFuncs, func(stats containerd.ProcessStatus) bool { + cl.StatusFilterFuncs = append(cl.StatusFilterFuncs, func(stats containerd.ProcessStatus) bool { return status == stats }) case containerd.ProcessStatus("exited"): - cl.statusFilterFuncs = append(cl.statusFilterFuncs, func(stats containerd.ProcessStatus) bool { + cl.StatusFilterFuncs = append(cl.StatusFilterFuncs, func(stats containerd.ProcessStatus) bool { return containerd.Stopped == stats }) case containerd.ProcessStatus("restarting"), containerd.ProcessStatus("removing"), containerd.ProcessStatus("dead"): @@ -127,28 +131,28 @@ func (cl *containerFilterContext) foldStatusFilter(_ context.Context, filter, va return nil } -func (cl *containerFilterContext) foldBeforeFilter(ctx context.Context, filter, value string) error { - beforeC, err := idOrNameFilter(ctx, cl.containers, value) +func (cl *FilterContext) foldBeforeFilter(ctx context.Context, filter, value string) error { + beforeC, err := idOrNameFilter(ctx, cl.Containers, value) if err == nil { - cl.beforeFilterFuncs = append(cl.beforeFilterFuncs, func(t time.Time) bool { + cl.BeforeFilterFuncs = append(cl.BeforeFilterFuncs, func(t time.Time) bool { return t.Before(beforeC.CreatedAt) }) } return err } -func (cl *containerFilterContext) foldSinceFilter(ctx context.Context, filter, value string) error { - sinceC, err := idOrNameFilter(ctx, cl.containers, value) +func (cl *FilterContext) foldSinceFilter(ctx context.Context, filter, value string) error { + sinceC, err := idOrNameFilter(ctx, cl.Containers, value) if err == nil { - cl.sinceFilterFuncs = append(cl.sinceFilterFuncs, func(t time.Time) bool { + cl.SinceFilterFuncs = append(cl.SinceFilterFuncs, func(t time.Time) bool { return t.After(sinceC.CreatedAt) }) } return err } -func (cl *containerFilterContext) foldIDFilter(_ context.Context, filter, value string) error { - cl.idFilterFuncs = append(cl.idFilterFuncs, func(id string) bool { +func (cl *FilterContext) foldIDFilter(_ context.Context, filter, value string) error { + cl.IDFilterFuncs = append(cl.IDFilterFuncs, func(id string) bool { if value == "" { return false } @@ -157,8 +161,8 @@ func (cl *containerFilterContext) foldIDFilter(_ context.Context, filter, value return nil } -func (cl *containerFilterContext) foldNameFilter(_ context.Context, filter, value string) error { - cl.nameFilterFuncs = append(cl.nameFilterFuncs, func(name string) bool { +func (cl *FilterContext) foldNameFilter(_ context.Context, filter, value string) error { + cl.NameFilterFuncs = append(cl.NameFilterFuncs, func(name string) bool { if value == "" { return true } @@ -167,13 +171,13 @@ func (cl *containerFilterContext) foldNameFilter(_ context.Context, filter, valu return nil } -func (cl *containerFilterContext) foldLabelFilter(_ context.Context, filter, value string) error { +func (cl *FilterContext) foldLabelFilter(_ context.Context, filter, value string) error { k, v, hasValue := value, "", false if subs := strings.SplitN(value, "=", 2); len(subs) == 2 { hasValue = true k, v = subs[0], subs[1] } - cl.labelFilterFuncs = append(cl.labelFilterFuncs, func(labels map[string]string) bool { + cl.LabelFilterFuncs = append(cl.LabelFilterFuncs, func(labels map[string]string) bool { if labels == nil { return false } @@ -186,8 +190,8 @@ func (cl *containerFilterContext) foldLabelFilter(_ context.Context, filter, val return nil } -func (cl *containerFilterContext) foldVolumeFilter(_ context.Context, filter, value string) error { - cl.volumeFilterFuncs = append(cl.volumeFilterFuncs, func(vols []*containerVolume) bool { +func (cl *FilterContext) foldVolumeFilter(_ context.Context, filter, value string) error { + cl.VolumeFilterFuncs = append(cl.VolumeFilterFuncs, func(vols []*volume.ContainerVolume) bool { for _, vol := range vols { if (vol.Source != "" && vol.Source == value) || (vol.Destination != "" && vol.Destination == value) || @@ -200,8 +204,8 @@ func (cl *containerFilterContext) foldVolumeFilter(_ context.Context, filter, va return nil } -func (cl *containerFilterContext) foldNetworkFilter(_ context.Context, filter, value string) error { - cl.networkFilterFuncs = append(cl.networkFilterFuncs, func(networks []string) bool { +func (cl *FilterContext) foldNetworkFilter(_ context.Context, filter, value string) error { + cl.NetworkFilterFuncs = append(cl.NetworkFilterFuncs, func(networks []string) bool { for _, network := range networks { if network == value { return true @@ -212,9 +216,9 @@ func (cl *containerFilterContext) foldNetworkFilter(_ context.Context, filter, v return nil } -func (cl *containerFilterContext) matchesInfoFilters(ctx context.Context, container containerd.Container) bool { - if len(cl.idFilterFuncs)+len(cl.nameFilterFuncs)+len(cl.beforeFilterFuncs)+ - len(cl.sinceFilterFuncs)+len(cl.labelFilterFuncs)+len(cl.volumeFilterFuncs)+len(cl.networkFilterFuncs) == 0 { +func (cl *FilterContext) matchesInfoFilters(ctx context.Context, container containerd.Container) bool { + if len(cl.IDFilterFuncs)+len(cl.NameFilterFuncs)+len(cl.BeforeFilterFuncs)+ + len(cl.SinceFilterFuncs)+len(cl.LabelFilterFuncs)+len(cl.VolumeFilterFuncs)+len(cl.NetworkFilterFuncs) == 0 { return true } info, _ := container.Info(ctx, containerd.WithoutRefreshedMetadata) @@ -223,8 +227,8 @@ func (cl *containerFilterContext) matchesInfoFilters(ctx context.Context, contai cl.matchesNetworkFilter(info) } -func (cl *containerFilterContext) matchesTaskFilters(ctx context.Context, container containerd.Container) bool { - if len(cl.exitedFilterFuncs)+len(cl.statusFilterFuncs) == 0 { +func (cl *FilterContext) matchesTaskFilters(ctx context.Context, container containerd.Container) bool { + if len(cl.ExitedFilterFuncs)+len(cl.StatusFilterFuncs) == 0 { return true } ctx, cancel := context.WithTimeout(ctx, 5*time.Second) @@ -242,14 +246,14 @@ func (cl *containerFilterContext) matchesTaskFilters(ctx context.Context, contai return cl.matchesExitedFilter(status) && cl.matchesStatusFilter(status) } -func (cl *containerFilterContext) matchesExitedFilter(status containerd.Status) bool { - if len(cl.exitedFilterFuncs) == 0 { +func (cl *FilterContext) matchesExitedFilter(status containerd.Status) bool { + if len(cl.ExitedFilterFuncs) == 0 { return true } if status.Status != containerd.Stopped { return false } - for _, exitedFilterFunc := range cl.exitedFilterFuncs { + for _, exitedFilterFunc := range cl.ExitedFilterFuncs { if !exitedFilterFunc(int(status.ExitStatus)) { continue } @@ -258,11 +262,11 @@ func (cl *containerFilterContext) matchesExitedFilter(status containerd.Status) return false } -func (cl *containerFilterContext) matchesStatusFilter(status containerd.Status) bool { - if len(cl.statusFilterFuncs) == 0 { +func (cl *FilterContext) matchesStatusFilter(status containerd.Status) bool { + if len(cl.StatusFilterFuncs) == 0 { return true } - for _, statusFilterFunc := range cl.statusFilterFuncs { + for _, statusFilterFunc := range cl.StatusFilterFuncs { if !statusFilterFunc(status.Status) { continue } @@ -271,11 +275,11 @@ func (cl *containerFilterContext) matchesStatusFilter(status containerd.Status) return false } -func (cl *containerFilterContext) matchesIDFilter(info containers.Container) bool { - if len(cl.idFilterFuncs) == 0 { +func (cl *FilterContext) matchesIDFilter(info containers.Container) bool { + if len(cl.IDFilterFuncs) == 0 { return true } - for _, idFilterFunc := range cl.idFilterFuncs { + for _, idFilterFunc := range cl.IDFilterFuncs { if !idFilterFunc(info.ID) { continue } @@ -284,12 +288,12 @@ func (cl *containerFilterContext) matchesIDFilter(info containers.Container) boo return false } -func (cl *containerFilterContext) matchesNameFilter(info containers.Container) bool { - if len(cl.nameFilterFuncs) == 0 { +func (cl *FilterContext) matchesNameFilter(info containers.Container) bool { + if len(cl.NameFilterFuncs) == 0 { return true } - cName := getPrintableContainerName(info.Labels) - for _, nameFilterFunc := range cl.nameFilterFuncs { + cName := utils.PrintableContainerName(info.Labels) + for _, nameFilterFunc := range cl.NameFilterFuncs { if !nameFilterFunc(cName) { continue } @@ -298,11 +302,11 @@ func (cl *containerFilterContext) matchesNameFilter(info containers.Container) b return false } -func (cl *containerFilterContext) matchesSinceFilter(info containers.Container) bool { - if len(cl.sinceFilterFuncs) == 0 { +func (cl *FilterContext) matchesSinceFilter(info containers.Container) bool { + if len(cl.SinceFilterFuncs) == 0 { return true } - for _, sinceFilterFunc := range cl.sinceFilterFuncs { + for _, sinceFilterFunc := range cl.SinceFilterFuncs { if !sinceFilterFunc(info.CreatedAt) { continue } @@ -311,11 +315,11 @@ func (cl *containerFilterContext) matchesSinceFilter(info containers.Container) return false } -func (cl *containerFilterContext) matchesBeforeFilter(info containers.Container) bool { - if len(cl.beforeFilterFuncs) == 0 { +func (cl *FilterContext) matchesBeforeFilter(info containers.Container) bool { + if len(cl.BeforeFilterFuncs) == 0 { return true } - for _, beforeFilterFunc := range cl.beforeFilterFuncs { + for _, beforeFilterFunc := range cl.BeforeFilterFuncs { if !beforeFilterFunc(info.CreatedAt) { continue } @@ -324,8 +328,8 @@ func (cl *containerFilterContext) matchesBeforeFilter(info containers.Container) return false } -func (cl *containerFilterContext) matchesLabelFilter(info containers.Container) bool { - for _, labelFilterFunc := range cl.labelFilterFuncs { +func (cl *FilterContext) matchesLabelFilter(info containers.Container) bool { + for _, labelFilterFunc := range cl.LabelFilterFuncs { if !labelFilterFunc(info.Labels) { return false } @@ -333,12 +337,12 @@ func (cl *containerFilterContext) matchesLabelFilter(info containers.Container) return true } -func (cl *containerFilterContext) matchesVolumeFilter(info containers.Container) bool { - if len(cl.volumeFilterFuncs) == 0 { +func (cl *FilterContext) matchesVolumeFilter(info containers.Container) bool { + if len(cl.VolumeFilterFuncs) == 0 { return true } - vols := getContainerVolumes(info.Labels) - for _, volumeFilterFunc := range cl.volumeFilterFuncs { + vols := volume.ContainerVolumes(info.Labels) + for _, volumeFilterFunc := range cl.VolumeFilterFuncs { if !volumeFilterFunc(vols) { continue } @@ -347,12 +351,12 @@ func (cl *containerFilterContext) matchesVolumeFilter(info containers.Container) return false } -func (cl *containerFilterContext) matchesNetworkFilter(info containers.Container) bool { - if len(cl.networkFilterFuncs) == 0 { +func (cl *FilterContext) matchesNetworkFilter(info containers.Container) bool { + if len(cl.NetworkFilterFuncs) == 0 { return true } - networks := getContainerNetworks(info.Labels) - for _, networkFilterFunc := range cl.networkFilterFuncs { + networks := containerNetworks(info.Labels) + for _, networkFilterFunc := range cl.NetworkFilterFuncs { if !networkFilterFunc(networks) { continue } @@ -367,9 +371,19 @@ func idOrNameFilter(ctx context.Context, containers []containerd.Container, valu if err != nil { return nil, err } - if strings.HasPrefix(info.ID, value) || strings.Contains(getPrintableContainerName(info.Labels), value) { + if strings.HasPrefix(info.ID, value) || strings.Contains(utils.PrintableContainerName(info.Labels), value) { return &info, nil } } return nil, fmt.Errorf("no such container %s", value) } + +func containerNetworks(containerLables map[string]string) []string { + var networks []string + if names, ok := containerLables[labels.Networks]; ok { + if err := json.Unmarshal([]byte(names), &networks); err != nil { + logrus.Warn(err) + } + } + return networks +} diff --git a/cmd/nerdctl/fmtutil.go b/cmd/nerdctl/utils/fmtutil/fmtutil.go similarity index 84% rename from cmd/nerdctl/fmtutil.go rename to cmd/nerdctl/utils/fmtutil/fmtutil.go index cb52442f2e0..626111ea485 100644 --- a/cmd/nerdctl/fmtutil.go +++ b/cmd/nerdctl/utils/fmtutil/fmtutil.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package fmtutil import ( "bytes" @@ -32,7 +32,7 @@ type Flusher interface { Flush() error } -func formatLabels(labels map[string]string) string { +func FormatLabels(labels map[string]string) string { var res string for k, v := range labels { s := k + "=" + v @@ -45,13 +45,13 @@ func formatLabels(labels map[string]string) string { return res } -// formatSlice formats the slice with `--format` flag. +// FormatSlice formats the slice with `--format` flag. // // --format="" (default): JSON // --format='{{json .}}': JSON lines // -// formatSlice is expected to be only used for `nerdctl OBJECT inspect` commands. -func formatSlice(cmd *cobra.Command, x []interface{}) error { +// FormatSlice is expected to be only used for `nerdctl OBJECT inspect` commands. +func FormatSlice(cmd *cobra.Command, x []interface{}) error { var tmpl *template.Template format, err := cmd.Flags().GetString("format") if err != nil { @@ -68,7 +68,7 @@ func formatSlice(cmd *cobra.Command, x []interface{}) error { return errors.New("unsupported format: \"raw\", \"table\", and \"wide\"") default: var err error - tmpl, err = parseTemplate(format) + tmpl, err = ParseTemplate(format) if err != nil { return err } @@ -113,9 +113,9 @@ func tryRawFormat(b *bytes.Buffer, f interface{}, tmpl *template.Template) error return nil } -// parseTemplate wraps github.com/docker/cli/templates.Parse() to allow `json` as an alias of `{{json .}}`. -// parseTemplate can be removed when https://github.com/docker/cli/pull/3355 gets merged and tagged (Docker 22.XX). -func parseTemplate(format string) (*template.Template, error) { +// ParseTemplate wraps github.com/docker/cli/templates.Parse() to allow `json` as an alias of `{{json .}}`. +// ParseTemplate can be removed when https://github.com/docker/cli/pull/3355 gets merged and tagged (Docker 22.XX). +func ParseTemplate(format string) (*template.Template, error) { aliases := map[string]string{ "json": "{{json .}}", } diff --git a/cmd/nerdctl/utils/image.go b/cmd/nerdctl/utils/image.go new file mode 100644 index 00000000000..787192e25bf --- /dev/null +++ b/cmd/nerdctl/utils/image.go @@ -0,0 +1,113 @@ +/* + 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 utils + +import ( + "context" + "errors" + "fmt" + "io" + + "github.com/containerd/containerd" + "github.com/containerd/containerd/errdefs" + "github.com/containerd/containerd/images" + "github.com/containerd/containerd/images/archive" + "github.com/containerd/containerd/platforms" + "github.com/containerd/containerd/snapshots" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/common" + "github.com/opencontainers/image-spec/identity" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +func LoadImage(in io.Reader, cmd *cobra.Command, platMC platforms.MatchComparer, quiet bool) error { + // In addition to passing WithImagePlatform() to client.Import(), we also need to pass WithDefaultPlatform() to NewClient(). + // Otherwise unpacking may fail. + client, ctx, cancel, err := ncclient.New(cmd, containerd.WithDefaultPlatform(platMC)) + if err != nil { + return err + } + defer cancel() + + sn, err := cmd.Flags().GetString("snapshotter") + if err != nil { + return err + } + + r := &common.ReadCounter{Reader: in} + imgs, err := client.Import(ctx, r, containerd.WithDigestRef(archive.DigestTranslator(sn)), containerd.WithSkipDigestRef(func(name string) bool { return name != "" }), containerd.WithImportPlatform(platMC)) + if err != nil { + if r.N == 0 { + // Avoid confusing "unrecognized image format" + return errors.New("no image was built") + } + if errors.Is(err, images.ErrEmptyWalk) { + err = fmt.Errorf("%w (Hint: set `--platform=PLATFORM` or `--all-platforms`)", err) + } + return err + } + for _, img := range imgs { + image := containerd.NewImageWithPlatform(client, img, platMC) + + // TODO: Show unpack status + if !quiet { + fmt.Fprintf(cmd.OutOrStdout(), "unpacking %s (%s)...\n", img.Name, img.Target.Digest) + } + err = image.Unpack(ctx, sn) + if err != nil { + return err + } + if quiet { + fmt.Fprintln(cmd.OutOrStdout(), img.Target.Digest) + } else { + fmt.Fprintf(cmd.OutOrStdout(), "Loaded image: %s\n", img.Name) + } + } + + return nil +} + +// UnpackedImageSize is the size of the unpacked snapshots. +// Does not contain the size of the blobs in the content store. (Corresponds to Docker). +func UnpackedImageSize(ctx context.Context, s snapshots.Snapshotter, img containerd.Image) (int64, error) { + diffIDs, err := img.RootFS(ctx) + if err != nil { + return 0, err + } + + chainID := identity.ChainID(diffIDs).String() + usage, err := s.Usage(ctx, chainID) + if err != nil { + if errdefs.IsNotFound(err) { + logrus.WithError(err).Debugf("image %q seems not unpacked", img.Name()) + return 0, nil + } + return 0, err + } + + info, err := s.Stat(ctx, chainID) + if err != nil { + return 0, err + } + + //Add ChainID's parent usage to the total usage + if err := common.SnapshotKey(info.Parent).Add(ctx, s, &usage); err != nil { + return 0, err + } + return usage.Size, nil +} diff --git a/cmd/nerdctl/utils/image_encrypt.go b/cmd/nerdctl/utils/image_encrypt.go new file mode 100644 index 00000000000..80b7f47277f --- /dev/null +++ b/cmd/nerdctl/utils/image_encrypt.go @@ -0,0 +1,71 @@ +/* + 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 utils + +import ( + "os" + "os/exec" + "path/filepath" + "testing" + + "github.com/containerd/nerdctl/pkg/buildkitutil" + "github.com/containerd/nerdctl/pkg/testutil" + "gotest.tools/v3/assert" +) + +type JweKeyPair struct { + Prv string + Pub string + Cleanup func() +} + +func NewJWEKeyPair(t testing.TB) *JweKeyPair { + if _, err := exec.LookPath("openssl"); err != nil { + t.Skip(err) + } + td, err := os.MkdirTemp(t.TempDir(), "jwe-key-pair") + assert.NilError(t, err) + prv := filepath.Join(td, "mykey.pem") + pub := filepath.Join(td, "mypubkey.pem") + cmds := [][]string{ + // Exec openssl commands to ensure that nerdctl is compatible with the output of openssl commands. + // Do NOT refactor this function to use "crypto/rsa" stdlib. + {"openssl", "genrsa", "-out", prv}, + {"openssl", "rsa", "-in", prv, "-pubout", "-out", pub}, + } + for _, f := range cmds { + cmd := exec.Command(f[0], f[1:]...) + if out, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("failed to run %v: %v (%q)", cmd.Args, err, string(out)) + } + } + return &JweKeyPair{ + Prv: prv, + Pub: pub, + Cleanup: func() { + _ = os.RemoveAll(td) + }, + } +} + +func RmiAll(base *testutil.Base) { + imageIDs := base.Cmd("images", "--no-trunc", "-a", "-q").OutLines() + base.Cmd(append([]string{"rmi", "-f"}, imageIDs...)...).AssertOK() + if _, err := buildkitutil.GetBuildkitHost(testutil.Namespace); err == nil { + base.Cmd("builder", "prune").AssertOK() + } +} diff --git a/cmd/nerdctl/utils/print.go b/cmd/nerdctl/utils/print.go new file mode 100644 index 00000000000..504bcea9866 --- /dev/null +++ b/cmd/nerdctl/utils/print.go @@ -0,0 +1,42 @@ +/* + 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 utils + +import ( + "fmt" + + "github.com/containerd/nerdctl/pkg/labels" + "github.com/containerd/nerdctl/pkg/labels/k8slabels" +) + +func PrintableContainerName(containerLabels map[string]string) string { + if name, ok := containerLabels[labels.Name]; ok { + return name + } + + if ns, ok := containerLabels[k8slabels.PodNamespace]; ok { + if podName, ok := containerLabels[k8slabels.PodName]; ok { + if containerName, ok := containerLabels[k8slabels.ContainerName]; ok { + // Container + return fmt.Sprintf("k8s://%s/%s/%s", ns, podName, containerName) + } + // Pod sandbox + return fmt.Sprintf("k8s://%s/%s", ns, podName) + } + } + return "" +} diff --git a/cmd/nerdctl/utils/pull.go b/cmd/nerdctl/utils/pull.go new file mode 100644 index 00000000000..30386d34995 --- /dev/null +++ b/cmd/nerdctl/utils/pull.go @@ -0,0 +1,139 @@ +/* + 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 utils + +import ( + "context" + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "testing" + + "github.com/containerd/containerd" + "github.com/containerd/nerdctl/pkg/cosignutil" + "github.com/containerd/nerdctl/pkg/imgutil" + "github.com/containerd/nerdctl/pkg/ipfs" + "github.com/containerd/nerdctl/pkg/referenceutil" + httpapi "github.com/ipfs/go-ipfs-http-client" + v1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "gotest.tools/v3/assert" +) + +type CosignKeyPair struct { + PublicKey string + PrivateKey string + Cleanup func() +} + +func NewCosignKeyPair(t testing.TB, path string) *CosignKeyPair { + td, err := os.MkdirTemp(t.TempDir(), path) + assert.NilError(t, err) + + cmd := exec.Command("cosign", "generate-key-pair") + cmd.Dir = td + if out, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("failed to run %v: %v (%q)", cmd.Args, err, string(out)) + } + + publicKey := filepath.Join(td, "cosign.pub") + privateKey := filepath.Join(td, "cosign.key") + + return &CosignKeyPair{ + PublicKey: publicKey, + PrivateKey: privateKey, + Cleanup: func() { + _ = os.RemoveAll(td) + }, + } +} + +func EnsureImage(ctx context.Context, cmd *cobra.Command, client *containerd.Client, rawRef string, ocispecPlatforms []v1.Platform, + pull string, unpack *bool, quiet bool) (*imgutil.EnsuredImage, error) { + + var ensured *imgutil.EnsuredImage + snapshotter, err := cmd.Flags().GetString("snapshotter") + if err != nil { + return nil, err + } + insecureRegistry, err := cmd.Flags().GetBool("insecure-registry") + if err != nil { + return nil, err + } + hostsDirs, err := cmd.Flags().GetStringSlice("hosts-dir") + if err != nil { + return nil, err + } + verifier, err := cmd.Flags().GetString("verify") + if err != nil { + return nil, err + } + + if scheme, ref, err := referenceutil.ParseIPFSRefWithScheme(rawRef); err == nil { + if verifier != "none" { + return nil, errors.New("--verify flag is not supported on IPFS as of now") + } + + ipfsClient, err := httpapi.NewLocalApi() + if err != nil { + return nil, err + } + ensured, err = ipfs.EnsureImage(ctx, client, ipfsClient, cmd.OutOrStdout(), cmd.ErrOrStderr(), snapshotter, scheme, ref, + pull, ocispecPlatforms, unpack, quiet) + if err != nil { + return nil, err + } + return ensured, nil + } + + ref := rawRef + switch verifier { + case "cosign": + experimental, err := cmd.Flags().GetBool("experimental") + if err != nil { + return nil, err + } + + if !experimental { + return nil, fmt.Errorf("cosign only work with enable experimental feature") + } + + keyRef, err := cmd.Flags().GetString("cosign-key") + if err != nil { + return nil, err + } + + ref, err = cosignutil.VerifyCosign(ctx, rawRef, keyRef, hostsDirs) + if err != nil { + return nil, err + } + case "none": + logrus.Debugf("verification process skipped") + default: + return nil, fmt.Errorf("no verifier found: %s", verifier) + } + + ensured, err = imgutil.EnsureImage(ctx, client, cmd.OutOrStdout(), cmd.ErrOrStderr(), snapshotter, ref, + pull, insecureRegistry, hostsDirs, ocispecPlatforms, unpack, quiet) + if err != nil { + return nil, err + } + return ensured, err +} diff --git a/cmd/nerdctl/save_linux_test.go b/cmd/nerdctl/utils/save_linux.go similarity index 62% rename from cmd/nerdctl/save_linux_test.go rename to cmd/nerdctl/utils/save_linux.go index c2970a9f04c..ba6cfb4db44 100644 --- a/cmd/nerdctl/save_linux_test.go +++ b/cmd/nerdctl/utils/save_linux.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package utils import ( "encoding/json" @@ -23,32 +23,11 @@ import ( "os" "os/exec" "path/filepath" - "strings" - "testing" - "github.com/containerd/nerdctl/pkg/testutil" - - "gotest.tools/v3/assert" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/common" ) -func TestSave(t *testing.T) { - base := testutil.NewBase(t) - base.Cmd("pull", testutil.AlpineImage).AssertOK() - archiveTarPath := filepath.Join(t.TempDir(), "a.tar") - base.Cmd("save", "-o", archiveTarPath, testutil.AlpineImage).AssertOK() - rootfsPath := filepath.Join(t.TempDir(), "rootfs") - err := extractDockerArchive(archiveTarPath, rootfsPath) - assert.NilError(t, err) - etcOSReleasePath := filepath.Join(rootfsPath, "/etc/os-release") - etcOSReleaseBytes, err := os.ReadFile(etcOSReleasePath) - assert.NilError(t, err) - etcOSRelease := string(etcOSReleaseBytes) - t.Logf("read %q, extracted from %q", etcOSReleasePath, testutil.AlpineImage) - t.Log(etcOSRelease) - assert.Assert(t, strings.Contains(etcOSRelease, "Alpine")) -} - -func extractDockerArchive(archiveTarPath, rootfsPath string) error { +func ExtractDockerArchive(archiveTarPath, rootfsPath string) error { if err := os.MkdirAll(rootfsPath, 0755); err != nil { return err } @@ -65,7 +44,7 @@ func extractDockerArchive(archiveTarPath, rootfsPath string) error { if err != nil { return err } - var mani DockerArchiveManifestJSON + var mani common.DockerArchiveManifestJSON if err := json.Unmarshal(manifestJSONBytes, &mani); err != nil { return err } @@ -85,14 +64,6 @@ func extractDockerArchive(archiveTarPath, rootfsPath string) error { return nil } -type DockerArchiveManifestJSON []DockerArchiveManifestJSONEntry - -type DockerArchiveManifestJSONEntry struct { - Config string - RepoTags []string - Layers []string -} - func extractTarFile(dirPath, tarFilePath string) error { cmd := exec.Command("tar", "Cxf", dirPath, tarFilePath) if out, err := cmd.CombinedOutput(); err != nil { diff --git a/cmd/nerdctl/utils/volume/volume.go b/cmd/nerdctl/utils/volume/volume.go new file mode 100644 index 00000000000..c514f8f68e4 --- /dev/null +++ b/cmd/nerdctl/utils/volume/volume.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 volume + +import ( + "encoding/json" + + "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/pkg/inspecttypes/native" + "github.com/containerd/nerdctl/pkg/labels" + "github.com/containerd/nerdctl/pkg/mountutil/volumestore" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +// ContainerVolume is a misnomer. +// TODO: find a better name. +type ContainerVolume struct { + Type string + Name string + Source string + Destination string + Mode string + RW bool + Propagation string +} + +// Store returns a volume store +// that corresponds to a directory like `/var/lib/nerdctl/1935db59/volume/default` +func Store(cmd *cobra.Command) (volumestore.VolumeStore, error) { + ns, err := cmd.Flags().GetString("namespace") + if err != nil { + return nil, err + } + dataStore, err := client.DataStore(cmd) + if err != nil { + return nil, err + } + return volumestore.New(dataStore, ns) +} + +func Volumes(cmd *cobra.Command) (map[string]native.Volume, error) { + volStore, err := Store(cmd) + if err != nil { + return nil, err + } + volumeSize, err := cmd.Flags().GetBool("size") + if err != nil { + return nil, err + } + return volStore.List(volumeSize) +} + +// ContainerVolumes is a misnomer. +// TODO: find a better name. +func ContainerVolumes(containerLabels map[string]string) []*ContainerVolume { + var vols []*ContainerVolume + volLabels := []string{labels.AnonymousVolumes, labels.Mounts} + for _, volLabel := range volLabels { + names, ok := containerLabels[volLabel] + if !ok { + continue + } + var ( + volumes []*ContainerVolume + err error + ) + if volLabel == labels.Mounts { + err = json.Unmarshal([]byte(names), &volumes) + } + if volLabel == labels.AnonymousVolumes { + var anonymous []string + err = json.Unmarshal([]byte(names), &anonymous) + for _, anony := range anonymous { + volumes = append(volumes, &ContainerVolume{Name: anony}) + } + + } + if err != nil { + logrus.Warn(err) + } + vols = append(vols, volumes...) + } + return vols +} diff --git a/cmd/nerdctl/version.go b/cmd/nerdctl/version/version.go similarity index 91% rename from cmd/nerdctl/version.go rename to cmd/nerdctl/version/version.go index 15915173439..70fccb6220a 100644 --- a/cmd/nerdctl/version.go +++ b/cmd/nerdctl/version/version.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package version import ( "bytes" @@ -23,12 +23,14 @@ import ( "os" "text/template" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/fmtutil" "github.com/containerd/nerdctl/pkg/infoutil" "github.com/containerd/nerdctl/pkg/inspecttypes/dockercompat" "github.com/spf13/cobra" ) -func newVersionCommand() *cobra.Command { +func NewVersionCommand() *cobra.Command { var versionCommand = &cobra.Command{ Use: "version", Args: cobra.NoArgs, @@ -54,7 +56,7 @@ func versionAction(cmd *cobra.Command, args []string) error { } if format != "" { var err error - tmpl, err = parseTemplate(format) + tmpl, err = fmtutil.ParseTemplate(format) if err != nil { return err } @@ -101,7 +103,7 @@ func versionInfo(cmd *cobra.Command) (dockercompat.VersionInfo, error) { v := dockercompat.VersionInfo{ Client: infoutil.ClientVersion(), } - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return v, err } diff --git a/cmd/nerdctl/volume.go b/cmd/nerdctl/volume.go deleted file mode 100644 index 912f50a2e2b..00000000000 --- a/cmd/nerdctl/volume.go +++ /dev/null @@ -1,55 +0,0 @@ -/* - 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 main - -import ( - "github.com/containerd/nerdctl/pkg/mountutil/volumestore" - "github.com/spf13/cobra" -) - -func newVolumeCommand() *cobra.Command { - volumeCommand := &cobra.Command{ - Annotations: map[string]string{Category: Management}, - Use: "volume", - Short: "Manage volumes", - RunE: unknownSubcommandAction, - SilenceUsage: true, - SilenceErrors: true, - } - volumeCommand.AddCommand( - newVolumeLsCommand(), - newVolumeInspectCommand(), - newVolumeCreateCommand(), - newVolumeRmCommand(), - newVolumePruneCommand(), - ) - return volumeCommand -} - -// getVolumeStore returns a volume store -// that corresponds to a directory like `/var/lib/nerdctl/1935db59/volumes/default` -func getVolumeStore(cmd *cobra.Command) (volumestore.VolumeStore, error) { - ns, err := cmd.Flags().GetString("namespace") - if err != nil { - return nil, err - } - dataStore, err := getDataStore(cmd) - if err != nil { - return nil, err - } - return volumestore.New(dataStore, ns) -} diff --git a/cmd/nerdctl/volume/volume.go b/cmd/nerdctl/volume/volume.go new file mode 100644 index 00000000000..6119be176d9 --- /dev/null +++ b/cmd/nerdctl/volume/volume.go @@ -0,0 +1,42 @@ +/* + 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 volume + +import ( + "github.com/containerd/nerdctl/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/common" + "github.com/spf13/cobra" +) + +func NewVolumeCommand() *cobra.Command { + volumeCommand := &cobra.Command{ + Annotations: map[string]string{common.Category: common.Management}, + Use: "volume", + Short: "Manage volumes", + RunE: completion.UnknownSubcommandAction, + SilenceUsage: true, + SilenceErrors: true, + } + volumeCommand.AddCommand( + NewLsCommand(), + NewInspectCommand(), + NewCreateCommand(), + NewRmCommand(), + NewPruneCommand(), + ) + return volumeCommand +} diff --git a/cmd/nerdctl/volume_create.go b/cmd/nerdctl/volume/volume_create.go similarity index 86% rename from cmd/nerdctl/volume_create.go rename to cmd/nerdctl/volume/volume_create.go index db832c15df6..c3b7995c75d 100644 --- a/cmd/nerdctl/volume_create.go +++ b/cmd/nerdctl/volume/volume_create.go @@ -14,22 +14,24 @@ limitations under the License. */ -package main +package volume import ( "fmt" "github.com/containerd/containerd/identifiers" + "github.com/containerd/nerdctl/cmd/nerdctl/utils" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/volume" "github.com/containerd/nerdctl/pkg/strutil" "github.com/spf13/cobra" ) -func newVolumeCreateCommand() *cobra.Command { +func NewCreateCommand() *cobra.Command { volumeCreateCommand := &cobra.Command{ Use: "create [flags] VOLUME", Short: "Create a volume", - Args: IsExactArgs(1), + Args: utils.IsExactArgs(1), RunE: volumeCreateAction, SilenceUsage: true, SilenceErrors: true, @@ -44,7 +46,7 @@ func volumeCreateAction(cmd *cobra.Command, args []string) error { return fmt.Errorf("malformed name %s: %w", name, err) } - volStore, err := getVolumeStore(cmd) + volStore, err := volume.Store(cmd) if err != nil { return err } diff --git a/cmd/nerdctl/volume_inspect.go b/cmd/nerdctl/volume/volume_inspect.go similarity index 84% rename from cmd/nerdctl/volume_inspect.go rename to cmd/nerdctl/volume/volume_inspect.go index 8fc19b458b4..b01d74ccd63 100644 --- a/cmd/nerdctl/volume_inspect.go +++ b/cmd/nerdctl/volume/volume_inspect.go @@ -14,13 +14,16 @@ limitations under the License. */ -package main +package volume import ( + "github.com/containerd/nerdctl/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/fmtutil" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/volume" "github.com/spf13/cobra" ) -func newVolumeInspectCommand() *cobra.Command { +func NewInspectCommand() *cobra.Command { volumeInspectCommand := &cobra.Command{ Use: "inspect [flags] VOLUME [VOLUME...]", Short: "Display detailed information on one or more volumes", @@ -44,7 +47,7 @@ func volumeInspectAction(cmd *cobra.Command, args []string) error { return err } - volStore, err := getVolumeStore(cmd) + volStore, err := volume.Store(cmd) if err != nil { return err } @@ -58,10 +61,10 @@ func volumeInspectAction(cmd *cobra.Command, args []string) error { result[i] = vol } - return formatSlice(cmd, result) + return fmtutil.FormatSlice(cmd, result) } func volumeInspectShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // show volume names - return shellCompleteVolumeNames(cmd) + return completion.ShellCompleteVolumeNames(cmd) } diff --git a/cmd/nerdctl/volume_ls.go b/cmd/nerdctl/volume/volume_ls.go similarity index 93% rename from cmd/nerdctl/volume_ls.go rename to cmd/nerdctl/volume/volume_ls.go index 3f38c921447..212d135824c 100644 --- a/cmd/nerdctl/volume_ls.go +++ b/cmd/nerdctl/volume/volume_ls.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package volume import ( "bytes" @@ -26,13 +26,15 @@ import ( "text/template" "github.com/containerd/containerd/pkg/progress" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/fmtutil" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/volume" "github.com/containerd/nerdctl/pkg/inspecttypes/native" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) -func newVolumeLsCommand() *cobra.Command { +func NewLsCommand() *cobra.Command { volumeLsCommand := &cobra.Command{ Use: "ls", Aliases: []string{"list"}, @@ -116,13 +118,13 @@ func volumeLsAction(cmd *cobra.Command, args []string) error { return errors.New("format and quiet must not be specified together") } var err error - tmpl, err = parseTemplate(format) + tmpl, err = fmtutil.ParseTemplate(format) if err != nil { return err } } - vols, err := getVolumes(cmd) + vols, err := volume.Volumes(cmd) if err != nil { return err } @@ -139,7 +141,7 @@ func volumeLsAction(cmd *cobra.Command, args []string) error { Scope: "local", } if v.Labels != nil { - p.Labels = formatLabels(*v.Labels) + p.Labels = fmtutil.FormatLabels(*v.Labels) } if volumeSize { p.Size = progress.Bytes(v.Size).String() @@ -160,24 +162,12 @@ func volumeLsAction(cmd *cobra.Command, args []string) error { fmt.Fprintf(w, "%s\t%s\n", p.Name, p.Mountpoint) } } - if f, ok := w.(Flusher); ok { + if f, ok := w.(fmtutil.Flusher); ok { return f.Flush() } return nil } -func getVolumes(cmd *cobra.Command) (map[string]native.Volume, error) { - volStore, err := getVolumeStore(cmd) - if err != nil { - return nil, err - } - volumeSize, err := cmd.Flags().GetBool("size") - if err != nil { - return nil, err - } - return volStore.List(volumeSize) -} - func getVolumeFilterFuncs(filters []string) ([]func(*map[string]string) bool, []func(string) bool, []func(int64) bool, bool, error) { isFilter := len(filters) > 0 labelFilterFuncs := make([]func(*map[string]string) bool, 0) diff --git a/cmd/nerdctl/volume_prune.go b/cmd/nerdctl/volume/volume_prune.go similarity index 86% rename from cmd/nerdctl/volume_prune.go rename to cmd/nerdctl/volume/volume_prune.go index e6da1ceb345..6e3f24c6d3c 100644 --- a/cmd/nerdctl/volume_prune.go +++ b/cmd/nerdctl/volume/volume_prune.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package volume import ( "context" @@ -22,10 +22,12 @@ import ( "strings" "github.com/containerd/containerd" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/volume" "github.com/spf13/cobra" ) -func newVolumePruneCommand() *cobra.Command { +func NewPruneCommand() *cobra.Command { volumePruneCommand := &cobra.Command{ Use: "prune [flags]", Short: "Remove all unused local volumes", @@ -56,17 +58,17 @@ func volumePruneAction(cmd *cobra.Command, _ []string) error { } } - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } defer cancel() - return volumePrune(ctx, cmd, client) + return Prune(ctx, cmd, client) } -func volumePrune(ctx context.Context, cmd *cobra.Command, client *containerd.Client) error { - volStore, err := getVolumeStore(cmd) +func Prune(ctx context.Context, cmd *cobra.Command, client *containerd.Client) error { + volStore, err := volume.Store(cmd) if err != nil { return err } diff --git a/cmd/nerdctl/volume_rm.go b/cmd/nerdctl/volume/volume_rm.go similarity index 89% rename from cmd/nerdctl/volume_rm.go rename to cmd/nerdctl/volume/volume_rm.go index b1215c9f949..976bfa4c315 100644 --- a/cmd/nerdctl/volume_rm.go +++ b/cmd/nerdctl/volume/volume_rm.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package volume import ( "context" @@ -22,13 +22,16 @@ import ( "fmt" "github.com/containerd/containerd" + ncclient "github.com/containerd/nerdctl/cmd/nerdctl/client" + "github.com/containerd/nerdctl/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/volume" "github.com/containerd/nerdctl/pkg/inspecttypes/dockercompat" "github.com/containerd/nerdctl/pkg/labels" "github.com/containerd/nerdctl/pkg/mountutil" "github.com/spf13/cobra" ) -func newVolumeRmCommand() *cobra.Command { +func NewRmCommand() *cobra.Command { volumeRmCommand := &cobra.Command{ Use: "rm [flags] VOLUME [VOLUME...]", Aliases: []string{"remove"}, @@ -45,7 +48,7 @@ func newVolumeRmCommand() *cobra.Command { } func volumeRmAction(cmd *cobra.Command, args []string) error { - client, ctx, cancel, err := newClient(cmd) + client, ctx, cancel, err := ncclient.New(cmd) if err != nil { return err } @@ -54,7 +57,7 @@ func volumeRmAction(cmd *cobra.Command, args []string) error { if err != nil { return err } - volStore, err := getVolumeStore(cmd) + volStore, err := volume.Store(cmd) if err != nil { return err } @@ -87,7 +90,7 @@ func volumeRmAction(cmd *cobra.Command, args []string) error { func volumeRmShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // show volume names - return shellCompleteVolumeNames(cmd) + return completion.ShellCompleteVolumeNames(cmd) } func usedVolumes(ctx context.Context, containers []containerd.Container) (map[string]struct{}, error) { diff --git a/cmd/nerdctl/build_test.go b/integration/build_test.go similarity index 93% rename from cmd/nerdctl/build_test.go rename to integration/build_test.go index df0f600104f..db5873a80d6 100644 --- a/cmd/nerdctl/build_test.go +++ b/integration/build_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "fmt" @@ -23,6 +23,7 @@ import ( "strings" "testing" + "github.com/containerd/nerdctl/cmd/nerdctl/builder" "github.com/containerd/nerdctl/pkg/testutil" "gotest.tools/v3/assert" ) @@ -38,7 +39,7 @@ func TestBuild(t *testing.T) { CMD ["echo", "nerdctl-build-test-string"] `, testutil.CommonImage) - buildCtx, err := createBuildContext(dockerfile) + buildCtx, err := builder.CreateBuildContext(dockerfile) assert.NilError(t, err) defer os.RemoveAll(buildCtx) @@ -68,7 +69,7 @@ RUN echo hello > /hello CMD ["echo", "nerdctl-build-test-string"] `, testutil.CommonImage) - buildCtx, err := createBuildContext(dockerfile) + buildCtx, err := builder.CreateBuildContext(dockerfile) assert.NilError(t, err) defer os.RemoveAll(buildCtx) @@ -80,7 +81,7 @@ RUN echo hello2 > /hello2 CMD ["cat", "/hello2"] `, imageName) - buildCtx2, err := createBuildContext(dockerfile2) + buildCtx2, err := builder.CreateBuildContext(dockerfile2) assert.NilError(t, err) defer os.RemoveAll(buildCtx2) @@ -115,7 +116,7 @@ RUN echo hello2 > /hello2 CMD ["cat", "/hello2"] `, imageName) - buildCtx2, err := createBuildContext(dockerfile2) + buildCtx2, err := builder.CreateBuildContext(dockerfile2) assert.NilError(t, err) defer os.RemoveAll(buildCtx2) @@ -188,7 +189,7 @@ func TestBuildLocal(t *testing.T) { COPY %s /`, testFileName) - buildCtx, err := createBuildContext(dockerfile) + buildCtx, err := builder.CreateBuildContext(dockerfile) assert.NilError(t, err) defer os.RemoveAll(buildCtx) @@ -216,17 +217,6 @@ COPY %s /`, assert.Equal(t, string(data), testContent) } -func createBuildContext(dockerfile string) (string, error) { - tmpDir, err := os.MkdirTemp("", "nerdctl-build-test") - if err != nil { - return "", err - } - if err = os.WriteFile(filepath.Join(tmpDir, "Dockerfile"), []byte(dockerfile), 0644); err != nil { - return "", err - } - return tmpDir, nil -} - func TestBuildWithBuildArg(t *testing.T) { testutil.RequiresBuild(t) base := testutil.NewBase(t) @@ -240,7 +230,7 @@ ENV TEST_STRING=$TEST_STRING CMD echo $TEST_STRING `, testutil.CommonImage) - buildCtx, err := createBuildContext(dockerfile) + buildCtx, err := builder.CreateBuildContext(dockerfile) assert.NilError(t, err) defer os.RemoveAll(buildCtx) @@ -291,7 +281,7 @@ func TestBuildWithIIDFile(t *testing.T) { CMD ["echo", "nerdctl-build-test-string"] `, testutil.CommonImage) - buildCtx, err := createBuildContext(dockerfile) + buildCtx, err := builder.CreateBuildContext(dockerfile) assert.NilError(t, err) defer os.RemoveAll(buildCtx) fileName := filepath.Join(t.TempDir(), "id.txt") @@ -317,7 +307,7 @@ func TestBuildWithLabels(t *testing.T) { LABEL name=nerdctl-build-test-label `, testutil.CommonImage) - buildCtx, err := createBuildContext(dockerfile) + buildCtx, err := builder.CreateBuildContext(dockerfile) assert.NilError(t, err) defer os.RemoveAll(buildCtx) @@ -341,7 +331,7 @@ func TestBuildMultipleTags(t *testing.T) { CMD ["echo", "nerdctl-build-test-string"] `, testutil.CommonImage) - buildCtx, err := createBuildContext(dockerfile) + buildCtx, err := builder.CreateBuildContext(dockerfile) assert.NilError(t, err) defer os.RemoveAll(buildCtx) @@ -395,7 +385,7 @@ CMD ["echo", "dockerfile"] err = os.WriteFile(filepath.Join(tmpDir, "Containerfile"), []byte(containerfile), 0644) assert.NilError(t, err) - buildCtx, err := createBuildContext(dockerfile) + buildCtx, err := builder.CreateBuildContext(dockerfile) assert.NilError(t, err) defer os.RemoveAll(buildCtx) @@ -412,7 +402,7 @@ func TestBuildNoTag(t *testing.T) { dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "nerdctl-build-notag-string"] `, testutil.CommonImage) - buildCtx, err := createBuildContext(dockerfile) + buildCtx, err := builder.CreateBuildContext(dockerfile) assert.NilError(t, err) defer os.RemoveAll(buildCtx) diff --git a/cmd/nerdctl/builder_linux_test.go b/integration/builder_linux_test.go similarity index 89% rename from cmd/nerdctl/builder_linux_test.go rename to integration/builder_linux_test.go index 5376698242d..5b62adef9a9 100644 --- a/cmd/nerdctl/builder_linux_test.go +++ b/integration/builder_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "bytes" @@ -22,6 +22,7 @@ import ( "os" "testing" + "github.com/containerd/nerdctl/cmd/nerdctl/builder" "github.com/containerd/nerdctl/pkg/testutil" "gotest.tools/v3/assert" ) @@ -34,7 +35,7 @@ func TestBuilderDebug(t *testing.T) { CMD ["echo", "nerdctl-builder-debug-test-string"] `, testutil.CommonImage) - buildCtx, err := createBuildContext(dockerfile) + buildCtx, err := builder.CreateBuildContext(dockerfile) assert.NilError(t, err) defer os.RemoveAll(buildCtx) diff --git a/cmd/nerdctl/commit_test.go b/integration/commit_test.go similarity index 98% rename from cmd/nerdctl/commit_test.go rename to integration/commit_test.go index c48209f1d24..428494680fc 100644 --- a/cmd/nerdctl/commit_test.go +++ b/integration/commit_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "fmt" diff --git a/cmd/nerdctl/completion_linux_test.go b/integration/completion_linux_test.go similarity index 99% rename from cmd/nerdctl/completion_linux_test.go rename to integration/completion_linux_test.go index ccff901371e..e09dde25e85 100644 --- a/cmd/nerdctl/completion_linux_test.go +++ b/integration/completion_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "testing" diff --git a/cmd/nerdctl/compose_build_linux_test.go b/integration/compose_build_linux_test.go similarity index 99% rename from cmd/nerdctl/compose_build_linux_test.go rename to integration/compose_build_linux_test.go index ec6be8cb5b1..0ebf1f5af18 100644 --- a/cmd/nerdctl/compose_build_linux_test.go +++ b/integration/compose_build_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "fmt" diff --git a/cmd/nerdctl/compose_config_test.go b/integration/compose_config_test.go similarity index 99% rename from cmd/nerdctl/compose_config_test.go rename to integration/compose_config_test.go index e6b76f3eea8..7a832ed0097 100644 --- a/cmd/nerdctl/compose_config_test.go +++ b/integration/compose_config_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "fmt" diff --git a/cmd/nerdctl/compose_exec_linux_test.go b/integration/compose_exec_linux_test.go similarity index 99% rename from cmd/nerdctl/compose_exec_linux_test.go rename to integration/compose_exec_linux_test.go index 28d92d76efa..7d0d2e2172a 100644 --- a/cmd/nerdctl/compose_exec_linux_test.go +++ b/integration/compose_exec_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "errors" diff --git a/cmd/nerdctl/compose_images_linux_test.go b/integration/compose_images_linux_test.go similarity index 99% rename from cmd/nerdctl/compose_images_linux_test.go rename to integration/compose_images_linux_test.go index 0fd76840442..97f697dc8a3 100644 --- a/cmd/nerdctl/compose_images_linux_test.go +++ b/integration/compose_images_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "fmt" diff --git a/cmd/nerdctl/compose_kill_linux_test.go b/integration/compose_kill_linux_test.go similarity index 99% rename from cmd/nerdctl/compose_kill_linux_test.go rename to integration/compose_kill_linux_test.go index 420d57de0fe..5f273bc94bc 100644 --- a/cmd/nerdctl/compose_kill_linux_test.go +++ b/integration/compose_kill_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "fmt" diff --git a/cmd/nerdctl/compose_pause_linux_test.go b/integration/compose_pause_linux_test.go similarity index 99% rename from cmd/nerdctl/compose_pause_linux_test.go rename to integration/compose_pause_linux_test.go index b31dfd1bbfb..d085795b608 100644 --- a/cmd/nerdctl/compose_pause_linux_test.go +++ b/integration/compose_pause_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "fmt" diff --git a/cmd/nerdctl/compose_port_linux_test.go b/integration/compose_port_linux_test.go similarity index 99% rename from cmd/nerdctl/compose_port_linux_test.go rename to integration/compose_port_linux_test.go index f5240edc55b..1915483eb5f 100644 --- a/cmd/nerdctl/compose_port_linux_test.go +++ b/integration/compose_port_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "fmt" diff --git a/cmd/nerdctl/compose_ps_linux_test.go b/integration/compose_ps_linux_test.go similarity index 96% rename from cmd/nerdctl/compose_ps_linux_test.go rename to integration/compose_ps_linux_test.go index cbd4c8ccd44..cd054c05e21 100644 --- a/cmd/nerdctl/compose_ps_linux_test.go +++ b/integration/compose_ps_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "encoding/json" @@ -22,6 +22,7 @@ import ( "strings" "testing" + "github.com/containerd/nerdctl/cmd/nerdctl/compose" "github.com/containerd/nerdctl/pkg/testutil" ) @@ -72,7 +73,7 @@ volumes: assertHandler := func(svc string, count int, fields ...string) func(stdout string) error { return func(stdout string) error { // 1. check json output can be unmarshalled back to printables. - var printables []composeContainerPrintable + var printables []compose.ContainerPrintable if err := json.Unmarshal([]byte(stdout), &printables); err != nil { return fmt.Errorf("[service: %s]failed to unmarshal json output from `compose ps`: %s", svc, stdout) } diff --git a/cmd/nerdctl/compose_pull_linux_test.go b/integration/compose_pull_linux_test.go similarity index 98% rename from cmd/nerdctl/compose_pull_linux_test.go rename to integration/compose_pull_linux_test.go index d350892f1b3..7e5faf8f044 100644 --- a/cmd/nerdctl/compose_pull_linux_test.go +++ b/integration/compose_pull_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "fmt" diff --git a/cmd/nerdctl/compose_restart_linux_test.go b/integration/compose_restart_linux_test.go similarity index 99% rename from cmd/nerdctl/compose_restart_linux_test.go rename to integration/compose_restart_linux_test.go index dbf23f05fd4..0e4eb8ee8c8 100644 --- a/cmd/nerdctl/compose_restart_linux_test.go +++ b/integration/compose_restart_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "fmt" diff --git a/cmd/nerdctl/compose_rm_linux_test.go b/integration/compose_rm_linux_test.go similarity index 99% rename from cmd/nerdctl/compose_rm_linux_test.go rename to integration/compose_rm_linux_test.go index 0a1dc5d7b3f..56ad3c5d38b 100644 --- a/cmd/nerdctl/compose_rm_linux_test.go +++ b/integration/compose_rm_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "fmt" diff --git a/cmd/nerdctl/compose_run_linux_test.go b/integration/compose_run_linux_test.go similarity index 98% rename from cmd/nerdctl/compose_run_linux_test.go rename to integration/compose_run_linux_test.go index 8a4daba41e2..9e9c8c64a19 100644 --- a/cmd/nerdctl/compose_run_linux_test.go +++ b/integration/compose_run_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "fmt" @@ -24,6 +24,7 @@ import ( "testing" "time" + "github.com/containerd/nerdctl/cmd/nerdctl/utils" "github.com/containerd/nerdctl/pkg/testutil" "github.com/containerd/nerdctl/pkg/testutil/nettestutil" "github.com/containerd/nerdctl/pkg/testutil/testregistry" @@ -422,8 +423,8 @@ func TestComposePushAndPullWithCosignVerify(t *testing.T) { // set up cosign and local registry t.Setenv("COSIGN_PASSWORD", "1") - keyPair := newCosignKeyPair(t, "cosign-key-pair") - defer keyPair.cleanup() + keyPair := utils.NewCosignKeyPair(t, "cosign-key-pair") + defer keyPair.Cleanup() reg := testregistry.NewPlainHTTP(base, 5000) defer reg.Cleanup() @@ -466,8 +467,8 @@ services: x-nerdctl-sign: none entrypoint: - stty -`, imageSvc0, keyPair.publicKey, keyPair.privateKey, - imageSvc1, keyPair.privateKey, imageSvc2) +`, imageSvc0, keyPair.PublicKey, keyPair.PrivateKey, + imageSvc1, keyPair.PrivateKey, imageSvc2) dockerfile := fmt.Sprintf(`FROM %s`, testutil.AlpineImage) diff --git a/cmd/nerdctl/compose_stop_linux_test.go b/integration/compose_stop_linux_test.go similarity index 99% rename from cmd/nerdctl/compose_stop_linux_test.go rename to integration/compose_stop_linux_test.go index 949f046d524..eb7115bc847 100644 --- a/cmd/nerdctl/compose_stop_linux_test.go +++ b/integration/compose_stop_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "fmt" diff --git a/cmd/nerdctl/compose_top_linux_test.go b/integration/compose_top_linux_test.go similarity index 98% rename from cmd/nerdctl/compose_top_linux_test.go rename to integration/compose_top_linux_test.go index 38a94fa84e9..1f6a64e8747 100644 --- a/cmd/nerdctl/compose_top_linux_test.go +++ b/integration/compose_top_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "fmt" diff --git a/cmd/nerdctl/compose_up_linux_test.go b/integration/compose_up_linux_test.go similarity index 99% rename from cmd/nerdctl/compose_up_linux_test.go rename to integration/compose_up_linux_test.go index 9ba2cee293c..6c978154564 100644 --- a/cmd/nerdctl/compose_up_linux_test.go +++ b/integration/compose_up_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "fmt" @@ -34,43 +34,6 @@ import ( "gotest.tools/v3/assert" ) -func TestComposeUp(t *testing.T) { - base := testutil.NewBase(t) - testComposeUp(t, base, fmt.Sprintf(` -version: '3.1' - -services: - - wordpress: - image: %s - restart: always - ports: - - 8080:80 - environment: - WORDPRESS_DB_HOST: db - WORDPRESS_DB_USER: exampleuser - WORDPRESS_DB_PASSWORD: examplepass - WORDPRESS_DB_NAME: exampledb - volumes: - - wordpress:/var/www/html - - db: - image: %s - restart: always - environment: - MYSQL_DATABASE: exampledb - MYSQL_USER: exampleuser - MYSQL_PASSWORD: examplepass - MYSQL_RANDOM_ROOT_PASSWORD: '1' - volumes: - - db:/var/lib/mysql - -volumes: - wordpress: - db: -`, testutil.WordpressImage, testutil.MariaDBImage)) -} - func testComposeUp(t *testing.T, base *testutil.Base, dockerComposeYAML string) { comp := testutil.NewComposeDir(t, dockerComposeYAML) defer comp.CleanUp() @@ -122,6 +85,43 @@ func testComposeUp(t *testing.T, base *testutil.Base, dockerComposeYAML string) base.Cmd("network", "inspect", fmt.Sprintf("%s_default", projectName)).AssertFail() } +func TestComposeUp(t *testing.T) { + base := testutil.NewBase(t) + testComposeUp(t, base, fmt.Sprintf(` +version: '3.1' + +services: + + wordpress: + image: %s + restart: always + ports: + - 8080:80 + environment: + WORDPRESS_DB_HOST: db + WORDPRESS_DB_USER: exampleuser + WORDPRESS_DB_PASSWORD: examplepass + WORDPRESS_DB_NAME: exampledb + volumes: + - wordpress:/var/www/html + + db: + image: %s + restart: always + environment: + MYSQL_DATABASE: exampledb + MYSQL_USER: exampleuser + MYSQL_PASSWORD: examplepass + MYSQL_RANDOM_ROOT_PASSWORD: '1' + volumes: + - db:/var/lib/mysql + +volumes: + wordpress: + db: +`, testutil.WordpressImage, testutil.MariaDBImage)) +} + func TestComposeUpBuild(t *testing.T) { testutil.RequiresBuild(t) base := testutil.NewBase(t) diff --git a/cmd/nerdctl/compose_version_test.go b/integration/compose_version_test.go similarity index 98% rename from cmd/nerdctl/compose_version_test.go rename to integration/compose_version_test.go index e9786c932c3..4869b94db46 100644 --- a/cmd/nerdctl/compose_version_test.go +++ b/integration/compose_version_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "testing" diff --git a/cmd/nerdctl/container_inspect_linux_test.go b/integration/container_inspect_linux_test.go similarity index 99% rename from cmd/nerdctl/container_inspect_linux_test.go rename to integration/container_inspect_linux_test.go index fd2d3ec1211..b4cc9ce72a7 100644 --- a/cmd/nerdctl/container_inspect_linux_test.go +++ b/integration/container_inspect_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "fmt" diff --git a/cmd/nerdctl/container_prune_linux_test.go b/integration/container_prune_linux_test.go similarity index 98% rename from cmd/nerdctl/container_prune_linux_test.go rename to integration/container_prune_linux_test.go index bfcda034719..6e0fba49fa6 100644 --- a/cmd/nerdctl/container_prune_linux_test.go +++ b/integration/container_prune_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "testing" diff --git a/cmd/nerdctl/cp_linux_test.go b/integration/cp_linux_test.go similarity index 99% rename from cmd/nerdctl/cp_linux_test.go rename to integration/cp_linux_test.go index 5442ed53b30..f1b2b193703 100644 --- a/cmd/nerdctl/cp_linux_test.go +++ b/integration/cp_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "fmt" diff --git a/cmd/nerdctl/create_linux_test.go b/integration/create_linux_test.go similarity index 99% rename from cmd/nerdctl/create_linux_test.go rename to integration/create_linux_test.go index 693788729da..877d1c12541 100644 --- a/cmd/nerdctl/create_linux_test.go +++ b/integration/create_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "fmt" diff --git a/cmd/nerdctl/exec_linux_test.go b/integration/exec_linux_test.go similarity index 99% rename from cmd/nerdctl/exec_linux_test.go rename to integration/exec_linux_test.go index 37ffcb69d06..429202f6c0a 100644 --- a/cmd/nerdctl/exec_linux_test.go +++ b/integration/exec_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "testing" diff --git a/cmd/nerdctl/exec_test.go b/integration/exec_test.go similarity index 99% rename from cmd/nerdctl/exec_test.go rename to integration/exec_test.go index b96cc74756a..0862a6cd507 100644 --- a/cmd/nerdctl/exec_test.go +++ b/integration/exec_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "errors" diff --git a/cmd/nerdctl/image_convert_linux_test.go b/integration/image_convert_linux_test.go similarity index 99% rename from cmd/nerdctl/image_convert_linux_test.go rename to integration/image_convert_linux_test.go index 10e4611e92d..5b833812a4d 100644 --- a/cmd/nerdctl/image_convert_linux_test.go +++ b/integration/image_convert_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "fmt" diff --git a/cmd/nerdctl/image_convert_test.go b/integration/image_convert_test.go similarity index 98% rename from cmd/nerdctl/image_convert_test.go rename to integration/image_convert_test.go index 4853c1070ce..54c0c978bec 100644 --- a/cmd/nerdctl/image_convert_test.go +++ b/integration/image_convert_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "runtime" diff --git a/cmd/nerdctl/image_encrypt_linux_test.go b/integration/image_encrypt_linux_test.go similarity index 51% rename from cmd/nerdctl/image_encrypt_linux_test.go rename to integration/image_encrypt_linux_test.go index a1ed55a3c49..21dd6b5ae47 100644 --- a/cmd/nerdctl/image_encrypt_linux_test.go +++ b/integration/image_encrypt_linux_test.go @@ -14,68 +14,21 @@ limitations under the License. */ -package main +package integration import ( "fmt" - "os" - "os/exec" - "path/filepath" "testing" - "github.com/containerd/nerdctl/pkg/buildkitutil" + "github.com/containerd/nerdctl/cmd/nerdctl/utils" "github.com/containerd/nerdctl/pkg/testutil" "github.com/containerd/nerdctl/pkg/testutil/testregistry" - "gotest.tools/v3/assert" ) -type jweKeyPair struct { - prv string - pub string - cleanup func() -} - -func newJWEKeyPair(t testing.TB) *jweKeyPair { - if _, err := exec.LookPath("openssl"); err != nil { - t.Skip(err) - } - td, err := os.MkdirTemp(t.TempDir(), "jwe-key-pair") - assert.NilError(t, err) - prv := filepath.Join(td, "mykey.pem") - pub := filepath.Join(td, "mypubkey.pem") - cmds := [][]string{ - // Exec openssl commands to ensure that nerdctl is compatible with the output of openssl commands. - // Do NOT refactor this function to use "crypto/rsa" stdlib. - {"openssl", "genrsa", "-out", prv}, - {"openssl", "rsa", "-in", prv, "-pubout", "-out", pub}, - } - for _, f := range cmds { - cmd := exec.Command(f[0], f[1:]...) - if out, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("failed to run %v: %v (%q)", cmd.Args, err, string(out)) - } - } - return &jweKeyPair{ - prv: prv, - pub: pub, - cleanup: func() { - _ = os.RemoveAll(td) - }, - } -} - -func rmiAll(base *testutil.Base) { - imageIDs := base.Cmd("images", "--no-trunc", "-a", "-q").OutLines() - base.Cmd(append([]string{"rmi", "-f"}, imageIDs...)...).AssertOK() - if _, err := buildkitutil.GetBuildkitHost(testutil.Namespace); err == nil { - base.Cmd("builder", "prune").AssertOK() - } -} - func TestImageEncryptJWE(t *testing.T) { testutil.DockerIncompatible(t) - keyPair := newJWEKeyPair(t) - defer keyPair.cleanup() + keyPair := utils.NewJWEKeyPair(t) + defer keyPair.Cleanup() base := testutil.NewBase(t) tID := testutil.Identifier(t) reg := testregistry.NewPlainHTTP(base, 5000) @@ -83,16 +36,16 @@ func TestImageEncryptJWE(t *testing.T) { base.Cmd("pull", testutil.CommonImage).AssertOK() encryptImageRef := fmt.Sprintf("127.0.0.1:%d/%s:encrypted", reg.ListenPort, tID) defer base.Cmd("rmi", encryptImageRef).Run() - base.Cmd("image", "encrypt", "--recipient=jwe:"+keyPair.pub, testutil.CommonImage, encryptImageRef).AssertOK() + base.Cmd("image", "encrypt", "--recipient=jwe:"+keyPair.Pub, testutil.CommonImage, encryptImageRef).AssertOK() base.Cmd("image", "inspect", "--mode=native", "--format={{len .Index.Manifests}}", encryptImageRef).AssertOutExactly("1\n") base.Cmd("image", "inspect", "--mode=native", "--format={{json .Manifest.Layers}}", encryptImageRef).AssertOutContains("org.opencontainers.image.enc.keys.jwe") base.Cmd("push", encryptImageRef).AssertOK() // remove all local images (in the nerdctl-test namespace), to ensure that we do not have blobs of the original image. - rmiAll(base) - base.Cmd("pull", encryptImageRef).AssertFail() // defaults to --unpack=true, and fails due to missing prv key + utils.RmiAll(base) + base.Cmd("pull", encryptImageRef).AssertFail() // defaults to --unpack=true, and fails due to missing Prv key base.Cmd("pull", "--unpack=false", encryptImageRef).AssertOK() decryptImageRef := tID + ":decrypted" defer base.Cmd("rmi", decryptImageRef).Run() - base.Cmd("image", "decrypt", "--key="+keyPair.pub, encryptImageRef, decryptImageRef).AssertFail() // decryption needs prv key, not pub key - base.Cmd("image", "decrypt", "--key="+keyPair.prv, encryptImageRef, decryptImageRef).AssertOK() + base.Cmd("image", "decrypt", "--key="+keyPair.Pub, encryptImageRef, decryptImageRef).AssertFail() // decryption needs Prv key, not Pub key + base.Cmd("image", "decrypt", "--key="+keyPair.Prv, encryptImageRef, decryptImageRef).AssertOK() } diff --git a/cmd/nerdctl/image_inspect_test.go b/integration/image_inspect_test.go similarity index 98% rename from cmd/nerdctl/image_inspect_test.go rename to integration/image_inspect_test.go index 1daae324564..c9d2a7dd503 100644 --- a/cmd/nerdctl/image_inspect_test.go +++ b/integration/image_inspect_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "testing" diff --git a/cmd/nerdctl/image_prune_test.go b/integration/image_prune_test.go similarity index 92% rename from cmd/nerdctl/image_prune_test.go rename to integration/image_prune_test.go index 1683da76cb0..09a17e04c93 100644 --- a/cmd/nerdctl/image_prune_test.go +++ b/integration/image_prune_test.go @@ -14,13 +14,14 @@ limitations under the License. */ -package main +package integration import ( "fmt" "os" "testing" + "github.com/containerd/nerdctl/cmd/nerdctl/builder" "github.com/containerd/nerdctl/pkg/testutil" "gotest.tools/v3/assert" ) @@ -36,7 +37,7 @@ func TestImagePrune(t *testing.T) { dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "nerdctl-test-image-prune"]`, testutil.CommonImage) - buildCtx, err := createBuildContext(dockerfile) + buildCtx, err := builder.CreateBuildContext(dockerfile) assert.NilError(t, err) defer os.RemoveAll(buildCtx) diff --git a/cmd/nerdctl/images_test.go b/integration/images_test.go similarity index 97% rename from cmd/nerdctl/images_test.go rename to integration/images_test.go index dbe7ba413c3..4f5ef6184c8 100644 --- a/cmd/nerdctl/images_test.go +++ b/integration/images_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "fmt" @@ -22,6 +22,7 @@ import ( "strings" "testing" + "github.com/containerd/nerdctl/cmd/nerdctl/builder" "github.com/containerd/nerdctl/pkg/tabutil" "github.com/containerd/nerdctl/pkg/testutil" "gotest.tools/v3/assert" @@ -88,7 +89,7 @@ CMD ["echo", "nerdctl-build-test-string"] \n LABEL foo=bar LABEL version=0.1`, testutil.CommonImage) - buildCtx, err := createBuildContext(dockerfile) + buildCtx, err := builder.CreateBuildContext(dockerfile) assert.NilError(t, err) defer os.RemoveAll(buildCtx) base.Cmd("build", "-t", tempName, "-f", buildCtx+"/Dockerfile", buildCtx).AssertOK() diff --git a/cmd/nerdctl/info_test.go b/integration/info_test.go similarity index 98% rename from cmd/nerdctl/info_test.go rename to integration/info_test.go index 7606b0ed601..cfc7d7d5180 100644 --- a/cmd/nerdctl/info_test.go +++ b/integration/info_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "encoding/json" diff --git a/cmd/nerdctl/ipfs_build_linux_test.go b/integration/ipfs_build_linux_test.go similarity index 92% rename from cmd/nerdctl/ipfs_build_linux_test.go rename to integration/ipfs_build_linux_test.go index 4d953b44caf..bbdf718bff1 100644 --- a/cmd/nerdctl/ipfs_build_linux_test.go +++ b/integration/ipfs_build_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "fmt" @@ -22,6 +22,7 @@ import ( "strings" "testing" + "github.com/containerd/nerdctl/cmd/nerdctl/builder" "github.com/containerd/nerdctl/pkg/testutil" "gotest.tools/v3/assert" @@ -43,7 +44,7 @@ func TestIPFSBuild(t *testing.T) { CMD ["echo", "nerdctl-build-test-string"] `, ipfsCIDBase) - buildCtx, err := createBuildContext(dockerfile) + buildCtx, err := builder.CreateBuildContext(dockerfile) assert.NilError(t, err) defer os.RemoveAll(buildCtx) diff --git a/cmd/nerdctl/ipfs_compose_linux_test.go b/integration/ipfs_compose_linux_test.go similarity index 98% rename from cmd/nerdctl/ipfs_compose_linux_test.go rename to integration/ipfs_compose_linux_test.go index 0114323690c..a0661d3dde9 100644 --- a/cmd/nerdctl/ipfs_compose_linux_test.go +++ b/integration/ipfs_compose_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "fmt" @@ -52,7 +52,7 @@ func TestIPFSComposeUp(t *testing.T) { t.Run(tt.name, func(t *testing.T) { base := testutil.NewBase(t) if tt.requiresStargz { - requiresStargz(base) + testutil.RequireStargz(base) } ipfsImgs := make([]string, 2) for i, img := range []string{testutil.WordpressImage, testutil.MariaDBImage} { diff --git a/cmd/nerdctl/ipfs_linux_test.go b/integration/ipfs_linux_test.go similarity index 92% rename from cmd/nerdctl/ipfs_linux_test.go rename to integration/ipfs_linux_test.go index 026768478ff..391af52bb86 100644 --- a/cmd/nerdctl/ipfs_linux_test.go +++ b/integration/ipfs_linux_test.go @@ -14,13 +14,14 @@ limitations under the License. */ -package main +package integration import ( "fmt" "os" "testing" + "github.com/containerd/nerdctl/cmd/nerdctl/utils" "github.com/containerd/nerdctl/pkg/infoutil" "github.com/containerd/nerdctl/pkg/rootlessutil" "github.com/containerd/nerdctl/pkg/testutil" @@ -40,23 +41,23 @@ func TestIPFS(t *testing.T) { base.Cmd("run", "--rm", ipfsCID, "echo", "hello").AssertOK() // encryption - keyPair := newJWEKeyPair(t) - defer keyPair.cleanup() + keyPair := utils.NewJWEKeyPair(t) + defer keyPair.Cleanup() tID := testutil.Identifier(t) encryptImageRef := tID + ":enc" layersNum := 1 - base.Cmd("image", "encrypt", "--recipient=jwe:"+keyPair.pub, ipfsCID, encryptImageRef).AssertOK() + base.Cmd("image", "encrypt", "--recipient=jwe:"+keyPair.Pub, ipfsCID, encryptImageRef).AssertOK() base.Cmd("image", "inspect", "--mode=native", "--format={{len .Manifest.Layers}}", encryptImageRef).AssertOutExactly(fmt.Sprintf("%d\n", layersNum)) for i := 0; i < layersNum; i++ { base.Cmd("image", "inspect", "--mode=native", fmt.Sprintf("--format={{json (index .Manifest.Layers %d) }}", i), encryptImageRef).AssertOutContains("org.opencontainers.image.enc.keys.jwe") } ipfsCIDEnc := cidOf(t, base.Cmd("push", "ipfs://"+encryptImageRef).OutLines()) - rmiAll(base) + utils.RmiAll(base) decryptImageRef := tID + ":dec" base.Cmd("pull", "--unpack=false", ipfsCIDEnc).AssertOK() - base.Cmd("image", "decrypt", "--key="+keyPair.pub, ipfsCIDEnc, decryptImageRef).AssertFail() // decryption needs prv key, not pub key - base.Cmd("image", "decrypt", "--key="+keyPair.prv, ipfsCIDEnc, decryptImageRef).AssertOK() + base.Cmd("image", "decrypt", "--key="+keyPair.Pub, ipfsCIDEnc, decryptImageRef).AssertFail() // decryption needs prv key, not pub key + base.Cmd("image", "decrypt", "--key="+keyPair.Prv, ipfsCIDEnc, decryptImageRef).AssertOK() base.Cmd("run", "--rm", decryptImageRef, "/bin/sh", "-c", "echo hello").AssertOK() } @@ -80,7 +81,7 @@ func TestIPFSCommit(t *testing.T) { base.Cmd("kill", newContainer).AssertOK() base.Cmd("rm", newContainer).AssertOK() ipfsCID2 := cidOf(t, base.Cmd("push", "ipfs://"+newImg).OutLines()) - rmiAll(base) + utils.RmiAll(base) base.Cmd("pull", ipfsCID2).AssertOK() base.Cmd("run", "--rm", ipfsCID2, "/bin/sh", "-c", "cat /hello").AssertOK() } @@ -89,7 +90,7 @@ func TestIPFSWithLazyPulling(t *testing.T) { requiresIPFS(t) testutil.DockerIncompatible(t) base := testutil.NewBase(t) - requiresStargz(base) + testutil.RequireStargz(base) ipfsCID := pushImageToIPFS(t, base, testutil.AlpineImage, "--estargz") base.Env = append(os.Environ(), "CONTAINERD_SNAPSHOTTER=stargz") @@ -105,7 +106,7 @@ func TestIPFSWithLazyPullingCommit(t *testing.T) { } testutil.DockerIncompatible(t) base := testutil.NewBase(t) - requiresStargz(base) + testutil.RequireStargz(base) ipfsCID := pushImageToIPFS(t, base, testutil.AlpineImage, "--estargz") base.Env = append(os.Environ(), "CONTAINERD_SNAPSHOTTER=stargz") @@ -118,7 +119,7 @@ func TestIPFSWithLazyPullingCommit(t *testing.T) { base.Cmd("kill", newContainer).AssertOK() base.Cmd("rm", newContainer).AssertOK() ipfsCID2 := cidOf(t, base.Cmd("push", "--estargz", "ipfs://"+newImg).OutLines()) - rmiAll(base) + utils.RmiAll(base) base.Cmd("pull", ipfsCID2).AssertOK() base.Cmd("run", "--rm", ipfsCID2, "/bin/sh", "-c", "ls /.stargz-snapshotter && cat /hello").AssertOK() diff --git a/cmd/nerdctl/ipfs_registry_linux_test.go b/integration/ipfs_registry_linux_test.go similarity index 97% rename from cmd/nerdctl/ipfs_registry_linux_test.go rename to integration/ipfs_registry_linux_test.go index dd4066322e6..004878ea10b 100644 --- a/cmd/nerdctl/ipfs_registry_linux_test.go +++ b/integration/ipfs_registry_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "os" @@ -45,7 +45,7 @@ func TestIPFSRegistryWithLazyPulling(t *testing.T) { testutil.DockerIncompatible(t) base := testutil.NewBase(t) - requiresStargz(base) + testutil.RequireStargz(base) base.Env = append(os.Environ(), "CONTAINERD_SNAPSHOTTER=stargz") ipfsCID := pushImageToIPFS(t, base, testutil.AlpineImage, "--estargz") ipfsRegistryAddr := "localhost:5555" diff --git a/cmd/nerdctl/load_linux_test.go b/integration/load_linux_test.go similarity index 98% rename from cmd/nerdctl/load_linux_test.go rename to integration/load_linux_test.go index 9681d1917bd..2deeee70494 100644 --- a/cmd/nerdctl/load_linux_test.go +++ b/integration/load_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "fmt" diff --git a/cmd/nerdctl/login_linux_test.go b/integration/login_linux_test.go similarity index 99% rename from cmd/nerdctl/login_linux_test.go rename to integration/login_linux_test.go index 12fbc8d4377..5b6cf663080 100644 --- a/cmd/nerdctl/login_linux_test.go +++ b/integration/login_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "fmt" diff --git a/cmd/nerdctl/logs_test.go b/integration/logs_test.go similarity index 99% rename from cmd/nerdctl/logs_test.go rename to integration/logs_test.go index 1643c13c817..2a389c38a67 100644 --- a/cmd/nerdctl/logs_test.go +++ b/integration/logs_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "fmt" diff --git a/cmd/nerdctl/main_linux_test.go b/integration/main_linux_test.go similarity index 98% rename from cmd/nerdctl/main_linux_test.go rename to integration/main_linux_test.go index a61fa50ee2b..e0d9272fd2e 100644 --- a/cmd/nerdctl/main_linux_test.go +++ b/integration/main_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "testing" diff --git a/cmd/nerdctl/main_test.go b/integration/main_test.go similarity index 99% rename from cmd/nerdctl/main_test.go rename to integration/main_test.go index 03a09360147..7016444795d 100644 --- a/cmd/nerdctl/main_test.go +++ b/integration/main_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "os" diff --git a/cmd/nerdctl/multi_platform_linux_test.go b/integration/multi_platform_linux_test.go similarity index 96% rename from cmd/nerdctl/multi_platform_linux_test.go rename to integration/multi_platform_linux_test.go index 017d2827c8b..f2854ef4a45 100644 --- a/cmd/nerdctl/multi_platform_linux_test.go +++ b/integration/multi_platform_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "fmt" @@ -23,6 +23,7 @@ import ( "strings" "testing" + "github.com/containerd/nerdctl/cmd/nerdctl/builder" "github.com/containerd/nerdctl/pkg/testutil" "github.com/containerd/nerdctl/pkg/testutil/nettestutil" "github.com/containerd/nerdctl/pkg/testutil/testregistry" @@ -68,7 +69,7 @@ func TestMultiPlatformBuildPush(t *testing.T) { RUN echo dummy `, testutil.AlpineImage) - buildCtx, err := createBuildContext(dockerfile) + buildCtx, err := builder.CreateBuildContext(dockerfile) assert.NilError(t, err) defer os.RemoveAll(buildCtx) @@ -97,7 +98,7 @@ func TestMultiPlatformBuildPushNoRun(t *testing.T) { CMD echo dummy `, testutil.AlpineImage) - buildCtx, err := createBuildContext(dockerfile) + buildCtx, err := builder.CreateBuildContext(dockerfile) assert.NilError(t, err) defer os.RemoveAll(buildCtx) diff --git a/cmd/nerdctl/network_create_linux_test.go b/integration/network_create_linux_test.go similarity index 98% rename from cmd/nerdctl/network_create_linux_test.go rename to integration/network_create_linux_test.go index d055c10bb28..2fc136f1084 100644 --- a/cmd/nerdctl/network_create_linux_test.go +++ b/integration/network_create_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "testing" diff --git a/cmd/nerdctl/network_inspect_test.go b/integration/network_inspect_test.go similarity index 98% rename from cmd/nerdctl/network_inspect_test.go rename to integration/network_inspect_test.go index 76b93ec7097..f2dd6b2e343 100644 --- a/cmd/nerdctl/network_inspect_test.go +++ b/integration/network_inspect_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "runtime" diff --git a/cmd/nerdctl/network_prune_linux_test.go b/integration/network_prune_linux_test.go similarity index 98% rename from cmd/nerdctl/network_prune_linux_test.go rename to integration/network_prune_linux_test.go index d872d540b26..ab974634f35 100644 --- a/cmd/nerdctl/network_prune_linux_test.go +++ b/integration/network_prune_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "testing" diff --git a/cmd/nerdctl/network_rm_linux_test.go b/integration/network_rm_linux_test.go similarity index 99% rename from cmd/nerdctl/network_rm_linux_test.go rename to integration/network_rm_linux_test.go index 9003f2a4bcf..03f913c0613 100644 --- a/cmd/nerdctl/network_rm_linux_test.go +++ b/integration/network_rm_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "testing" diff --git a/cmd/nerdctl/ps_linux_test.go b/integration/ps_linux_test.go similarity index 99% rename from cmd/nerdctl/ps_linux_test.go rename to integration/ps_linux_test.go index d7b04a3f005..ebb326059a5 100644 --- a/cmd/nerdctl/ps_linux_test.go +++ b/integration/ps_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "errors" diff --git a/cmd/nerdctl/pull_linux_test.go b/integration/pull_linux_test.go similarity index 74% rename from cmd/nerdctl/pull_linux_test.go rename to integration/pull_linux_test.go index 0051890899b..f3fb24b1be5 100644 --- a/cmd/nerdctl/pull_linux_test.go +++ b/integration/pull_linux_test.go @@ -14,49 +14,22 @@ limitations under the License. */ -package main +package integration import ( "fmt" "os" "os/exec" - "path/filepath" "strings" "testing" + "github.com/containerd/nerdctl/cmd/nerdctl/builder" + "github.com/containerd/nerdctl/cmd/nerdctl/utils" "github.com/containerd/nerdctl/pkg/testutil" "github.com/containerd/nerdctl/pkg/testutil/testregistry" "gotest.tools/v3/assert" ) -type cosignKeyPair struct { - publicKey string - privateKey string - cleanup func() -} - -func newCosignKeyPair(t testing.TB, path string) *cosignKeyPair { - td, err := os.MkdirTemp(t.TempDir(), path) - assert.NilError(t, err) - - cmd := exec.Command("cosign", "generate-key-pair") - cmd.Dir = td - if out, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("failed to run %v: %v (%q)", cmd.Args, err, string(out)) - } - - publicKey := filepath.Join(td, "cosign.pub") - privateKey := filepath.Join(td, "cosign.key") - - return &cosignKeyPair{ - publicKey: publicKey, - privateKey: privateKey, - cleanup: func() { - _ = os.RemoveAll(td) - }, - } -} - func TestImageVerifyWithCosign(t *testing.T) { if _, err := exec.LookPath("cosign"); err != nil { t.Skip() @@ -64,8 +37,8 @@ func TestImageVerifyWithCosign(t *testing.T) { testutil.DockerIncompatible(t) testutil.RequiresBuild(t) t.Setenv("COSIGN_PASSWORD", "1") - keyPair := newCosignKeyPair(t, "cosign-key-pair") - defer keyPair.cleanup() + keyPair := utils.NewCosignKeyPair(t, "cosign-key-pair") + defer keyPair.Cleanup() base := testutil.NewBase(t) defer base.Cmd("builder", "prune").Run() tID := testutil.Identifier(t) @@ -81,13 +54,13 @@ func TestImageVerifyWithCosign(t *testing.T) { CMD ["echo", "nerdctl-build-test-string"] `, testutil.CommonImage) - buildCtx, err := createBuildContext(dockerfile) + buildCtx, err := builder.CreateBuildContext(dockerfile) assert.NilError(t, err) defer os.RemoveAll(buildCtx) base.Cmd("build", "-t", testImageRef, buildCtx).AssertOK() - base.Cmd("push", testImageRef, "--sign=cosign", "--cosign-key="+keyPair.privateKey).AssertOK() - base.Cmd("pull", testImageRef, "--verify=cosign", "--cosign-key="+keyPair.publicKey).AssertOK() + base.Cmd("push", testImageRef, "--sign=cosign", "--cosign-key="+keyPair.PrivateKey).AssertOK() + base.Cmd("pull", testImageRef, "--verify=cosign", "--cosign-key="+keyPair.PublicKey).AssertOK() } func TestImagePullPlainHttpWithDefaultPort(t *testing.T) { @@ -105,7 +78,7 @@ func TestImagePullPlainHttpWithDefaultPort(t *testing.T) { CMD ["echo", "nerdctl-build-test-string"] `, testutil.CommonImage) - buildCtx, err := createBuildContext(dockerfile) + buildCtx, err := builder.CreateBuildContext(dockerfile) assert.NilError(t, err) defer os.RemoveAll(buildCtx) base.Cmd("build", "-t", testImageRef, buildCtx).AssertOK() @@ -120,8 +93,8 @@ func TestImageVerifyWithCosignShouldFailWhenKeyIsNotCorrect(t *testing.T) { testutil.DockerIncompatible(t) testutil.RequiresBuild(t) t.Setenv("COSIGN_PASSWORD", "1") - keyPair := newCosignKeyPair(t, "cosign-key-pair") - defer keyPair.cleanup() + keyPair := utils.NewCosignKeyPair(t, "cosign-key-pair") + defer keyPair.Cleanup() base := testutil.NewBase(t) defer base.Cmd("builder", "prune").Run() tID := testutil.Identifier(t) @@ -137,15 +110,15 @@ func TestImageVerifyWithCosignShouldFailWhenKeyIsNotCorrect(t *testing.T) { CMD ["echo", "nerdctl-build-test-string"] `, testutil.CommonImage) - buildCtx, err := createBuildContext(dockerfile) + buildCtx, err := builder.CreateBuildContext(dockerfile) assert.NilError(t, err) defer os.RemoveAll(buildCtx) base.Cmd("build", "-t", testImageRef, buildCtx).AssertOK() - base.Cmd("push", testImageRef, "--sign=cosign", "--cosign-key="+keyPair.privateKey).AssertOK() - base.Cmd("pull", testImageRef, "--verify=cosign", "--cosign-key="+keyPair.publicKey).AssertOK() + base.Cmd("push", testImageRef, "--sign=cosign", "--cosign-key="+keyPair.PrivateKey).AssertOK() + base.Cmd("pull", testImageRef, "--verify=cosign", "--cosign-key="+keyPair.PublicKey).AssertOK() t.Setenv("COSIGN_PASSWORD", "2") - newKeyPair := newCosignKeyPair(t, "cosign-key-pair-test") - base.Cmd("pull", testImageRef, "--verify=cosign", "--cosign-key="+newKeyPair.publicKey).AssertFail() + newKeyPair := utils.NewCosignKeyPair(t, "cosign-key-pair-test") + base.Cmd("pull", testImageRef, "--verify=cosign", "--cosign-key="+newKeyPair.PublicKey).AssertFail() } diff --git a/cmd/nerdctl/push_linux_test.go b/integration/push_linux_test.go similarity index 99% rename from cmd/nerdctl/push_linux_test.go rename to integration/push_linux_test.go index bfe8cb02bcf..0b4b0fb27c4 100644 --- a/cmd/nerdctl/push_linux_test.go +++ b/integration/push_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "fmt" diff --git a/cmd/nerdctl/rename_linux_test.go b/integration/rename_linux_test.go similarity index 99% rename from cmd/nerdctl/rename_linux_test.go rename to integration/rename_linux_test.go index b082514fe1c..9794ae6df41 100644 --- a/cmd/nerdctl/rename_linux_test.go +++ b/integration/rename_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "testing" diff --git a/cmd/nerdctl/restart_linux_test.go b/integration/restart_linux_test.go similarity index 99% rename from cmd/nerdctl/restart_linux_test.go rename to integration/restart_linux_test.go index b04e0346fc7..f8e272876bc 100644 --- a/cmd/nerdctl/restart_linux_test.go +++ b/integration/restart_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "fmt" diff --git a/cmd/nerdctl/rm_linux_test.go b/integration/rm_linux_test.go similarity index 98% rename from cmd/nerdctl/rm_linux_test.go rename to integration/rm_linux_test.go index 3020dfc42f4..6ea06395d60 100644 --- a/cmd/nerdctl/rm_linux_test.go +++ b/integration/rm_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "testing" diff --git a/cmd/nerdctl/rmi_linux_test.go b/integration/rmi_linux_test.go similarity index 98% rename from cmd/nerdctl/rmi_linux_test.go rename to integration/rmi_linux_test.go index ecc55b435f9..b8c1706f478 100644 --- a/cmd/nerdctl/rmi_linux_test.go +++ b/integration/rmi_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "testing" diff --git a/cmd/nerdctl/run_cgroup_linux_test.go b/integration/run_cgroup_linux_test.go similarity index 98% rename from cmd/nerdctl/run_cgroup_linux_test.go rename to integration/run_cgroup_linux_test.go index c05fea26c20..1b3b5d58346 100644 --- a/cmd/nerdctl/run_cgroup_linux_test.go +++ b/integration/run_cgroup_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "bytes" @@ -25,6 +25,7 @@ import ( "github.com/containerd/cgroups" "github.com/containerd/containerd/pkg/userns" "github.com/containerd/continuity/testutil/loopback" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/action/run" "github.com/containerd/nerdctl/pkg/testutil" "gotest.tools/v3/assert" ) @@ -252,7 +253,7 @@ func TestParseDevice(t *testing.T) { for _, tc := range testCases { t.Log(tc.s) - devPath, mode, err := parseDevice(tc.s) + devPath, mode, err := run.ParseDevice(tc.s) if tc.err == "" { assert.NilError(t, err) assert.Equal(t, tc.expectedDevPath, devPath) diff --git a/cmd/nerdctl/run_gpus_test.go b/integration/run_gpus_test.go similarity index 75% rename from cmd/nerdctl/run_gpus_test.go rename to integration/run_gpus_test.go index 537a3761e53..54b82320b03 100644 --- a/cmd/nerdctl/run_gpus_test.go +++ b/integration/run_gpus_test.go @@ -14,11 +14,12 @@ limitations under the License. */ -package main +package integration import ( "testing" + "github.com/containerd/nerdctl/cmd/nerdctl/utils/action/run" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -31,11 +32,11 @@ func TestParseGpusOptAll(t *testing.T) { "count=all", "count=-1", } { - req, err := parseGPUOptCSV(testcase) + req, err := run.ParseGPUOptCSV(testcase) assert.NilError(t, err) - assert.Equal(t, req.count, -1) - assert.Equal(t, len(req.deviceIDs), 0) - assert.Equal(t, len(req.capabilities), 0) + assert.Equal(t, req.Count, -1) + assert.Equal(t, len(req.DeviceIDs), 0) + assert.Equal(t, len(req.Capabilities), 0) } } @@ -48,10 +49,10 @@ func TestParseGpusOpts(t *testing.T) { "driver=nvidia,\"capabilities=compute,utility\",count=1", "\"capabilities=compute,utility\",count=1", } { - req, err := parseGPUOptCSV(testcase) + req, err := run.ParseGPUOptCSV(testcase) assert.NilError(t, err) - assert.Equal(t, req.count, 1) - assert.Equal(t, len(req.deviceIDs), 0) - assert.Check(t, is.DeepEqual(req.capabilities, []string{"compute", "utility"})) + assert.Equal(t, req.Count, 1) + assert.Equal(t, len(req.DeviceIDs), 0) + assert.Check(t, is.DeepEqual(req.Capabilities, []string{"compute", "utility"})) } } diff --git a/cmd/nerdctl/run_linux_test.go b/integration/run_linux_test.go similarity index 98% rename from cmd/nerdctl/run_linux_test.go rename to integration/run_linux_test.go index 63c082790ce..cca7f8bdf50 100644 --- a/cmd/nerdctl/run_linux_test.go +++ b/integration/run_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "bufio" @@ -29,6 +29,7 @@ import ( "testing" "time" + "github.com/containerd/nerdctl/cmd/nerdctl/utils" "github.com/containerd/nerdctl/pkg/rootlessutil" "github.com/containerd/nerdctl/pkg/strutil" "github.com/containerd/nerdctl/pkg/testutil" @@ -54,7 +55,7 @@ func prepareCustomRootfs(base *testutil.Base, imageName string) string { base.Cmd("save", "-o", archiveTarPath, imageName).AssertOK() rootfs, err := os.MkdirTemp(base.T.TempDir(), "rootfs") assert.NilError(base.T, err) - err = extractDockerArchive(archiveTarPath, rootfs) + err = utils.ExtractDockerArchive(archiveTarPath, rootfs) assert.NilError(base.T, err) return rootfs } diff --git a/cmd/nerdctl/run_log_driver_syslog_test.go b/integration/run_log_driver_syslog_test.go similarity index 99% rename from cmd/nerdctl/run_log_driver_syslog_test.go rename to integration/run_log_driver_syslog_test.go index e85a8c1a2a5..fee3005f590 100644 --- a/cmd/nerdctl/run_log_driver_syslog_test.go +++ b/integration/run_log_driver_syslog_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "fmt" diff --git a/cmd/nerdctl/run_mount_linux_test.go b/integration/run_mount_linux_test.go similarity index 98% rename from cmd/nerdctl/run_mount_linux_test.go rename to integration/run_mount_linux_test.go index 37a9d4670f0..5c603cdd754 100644 --- a/cmd/nerdctl/run_mount_linux_test.go +++ b/integration/run_mount_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "fmt" @@ -24,6 +24,7 @@ import ( "testing" "github.com/containerd/containerd/mount" + "github.com/containerd/nerdctl/cmd/nerdctl/builder" "github.com/containerd/nerdctl/pkg/rootlessutil" "github.com/containerd/nerdctl/pkg/testutil" mobymount "github.com/moby/sys/mount" @@ -101,7 +102,7 @@ func TestRunAnonymousVolumeWithBuild(t *testing.T) { VOLUME /foo `, testutil.AlpineImage) - buildCtx, err := createBuildContext(dockerfile) + buildCtx, err := builder.CreateBuildContext(dockerfile) assert.NilError(t, err) defer os.RemoveAll(buildCtx) @@ -125,7 +126,7 @@ RUN mkdir -p /mnt && echo hi > /mnt/initial_file CMD ["cat", "/mnt/initial_file"] `, testutil.AlpineImage) - buildCtx, err := createBuildContext(dockerfile) + buildCtx, err := builder.CreateBuildContext(dockerfile) assert.NilError(t, err) defer os.RemoveAll(buildCtx) @@ -155,7 +156,7 @@ VOLUME /mnt CMD ["cat", "/mnt/initial_file"] `, testutil.AlpineImage) - buildCtx, err := createBuildContext(dockerfile) + buildCtx, err := builder.CreateBuildContext(dockerfile) assert.NilError(t, err) defer os.RemoveAll(buildCtx) @@ -190,7 +191,7 @@ CMD ["readlink", "/mnt/passwd"] `, testutil.AlpineImage) const expected = "../../../../../../../../../../../../../../../../../../etc/passwd\n" - buildCtx, err := createBuildContext(dockerfile) + buildCtx, err := builder.CreateBuildContext(dockerfile) assert.NilError(t, err) defer os.RemoveAll(buildCtx) @@ -219,7 +220,7 @@ func TestRunCopyingUpInitialContentsShouldNotResetTheCopiedContents(t *testing.T RUN echo -n "rev0" > /mnt/file `, testutil.AlpineImage) - buildCtx, err := createBuildContext(dockerfile) + buildCtx, err := builder.CreateBuildContext(dockerfile) assert.NilError(t, err) defer os.RemoveAll(buildCtx) diff --git a/cmd/nerdctl/run_network_linux_test.go b/integration/run_network_linux_test.go similarity index 99% rename from cmd/nerdctl/run_network_linux_test.go rename to integration/run_network_linux_test.go index 0bf6bfcdb6d..097f6cc8b58 100644 --- a/cmd/nerdctl/run_network_linux_test.go +++ b/integration/run_network_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "fmt" diff --git a/cmd/nerdctl/run_restart_linux_test.go b/integration/run_restart_linux_test.go similarity index 99% rename from cmd/nerdctl/run_restart_linux_test.go rename to integration/run_restart_linux_test.go index b57d53a81d3..c9eadfb5c2f 100644 --- a/cmd/nerdctl/run_restart_linux_test.go +++ b/integration/run_restart_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "fmt" diff --git a/cmd/nerdctl/run_runtime_linux_test.go b/integration/run_runtime_linux_test.go similarity index 97% rename from cmd/nerdctl/run_runtime_linux_test.go rename to integration/run_runtime_linux_test.go index fc33c21004a..f36b3699040 100644 --- a/cmd/nerdctl/run_runtime_linux_test.go +++ b/integration/run_runtime_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "testing" diff --git a/cmd/nerdctl/run_security_linux_test.go b/integration/run_security_linux_test.go similarity index 99% rename from cmd/nerdctl/run_security_linux_test.go rename to integration/run_security_linux_test.go index 49db43cbfe2..9b6f0fb22d5 100644 --- a/cmd/nerdctl/run_security_linux_test.go +++ b/integration/run_security_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "fmt" diff --git a/cmd/nerdctl/run_stargz_linux_test.go b/integration/run_stargz_linux_test.go similarity index 81% rename from cmd/nerdctl/run_stargz_linux_test.go rename to integration/run_stargz_linux_test.go index 0e1c8266b3d..73ab068eba2 100644 --- a/cmd/nerdctl/run_stargz_linux_test.go +++ b/integration/run_stargz_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "testing" @@ -25,17 +25,7 @@ import ( func TestRunStargz(t *testing.T) { testutil.DockerIncompatible(t) base := testutil.NewBase(t) - requiresStargz(base) + testutil.RequireStargz(base) // if stargz snapshotter is functional, "/.stargz-snapshotter" appears base.Cmd("--snapshotter=stargz", "run", "--rm", testutil.FedoraESGZImage, "ls", "/.stargz-snapshotter").AssertOK() } - -func requiresStargz(base *testutil.Base) { - info := base.Info() - for _, p := range info.Plugins.Storage { - if p == "stargz" { - return - } - } - base.T.Skip("test requires stargz") -} diff --git a/cmd/nerdctl/run_test.go b/integration/run_test.go similarity index 98% rename from cmd/nerdctl/run_test.go rename to integration/run_test.go index 58ce840be10..ea2b9f2d7a4 100644 --- a/cmd/nerdctl/run_test.go +++ b/integration/run_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "errors" @@ -27,6 +27,7 @@ import ( "testing" "time" + "github.com/containerd/nerdctl/cmd/nerdctl/builder" "github.com/containerd/nerdctl/pkg/testutil" "gotest.tools/v3/assert" "gotest.tools/v3/icmd" @@ -46,7 +47,7 @@ ENTRYPOINT ["echo", "foo"] CMD ["echo", "bar"] `, testutil.CommonImage) - buildCtx, err := createBuildContext(dockerfile) + buildCtx, err := builder.CreateBuildContext(dockerfile) assert.NilError(t, err) defer os.RemoveAll(buildCtx) @@ -430,7 +431,7 @@ FROM scratch COPY --from=builder /go/src/logger/logger / ` - buildCtx, err := createBuildContext(dockerfile) + buildCtx, err := builder.CreateBuildContext(dockerfile) assert.NilError(t, err) defer os.RemoveAll(buildCtx) tmpDir := t.TempDir() diff --git a/cmd/nerdctl/run_user_linux_test.go b/integration/run_user_linux_test.go similarity index 99% rename from cmd/nerdctl/run_user_linux_test.go rename to integration/run_user_linux_test.go index 262fc7ebc45..5618a2ba351 100644 --- a/cmd/nerdctl/run_user_linux_test.go +++ b/integration/run_user_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "testing" diff --git a/cmd/nerdctl/run_user_windows_test.go b/integration/run_user_windows_test.go similarity index 98% rename from cmd/nerdctl/run_user_windows_test.go rename to integration/run_user_windows_test.go index 27cfd0697af..e4522bb3695 100644 --- a/cmd/nerdctl/run_user_windows_test.go +++ b/integration/run_user_windows_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "testing" diff --git a/cmd/nerdctl/run_verify_linux_test.go b/integration/run_verify_linux_test.go similarity index 84% rename from cmd/nerdctl/run_verify_linux_test.go rename to integration/run_verify_linux_test.go index 32098b9fa5d..38845cb9f79 100644 --- a/cmd/nerdctl/run_verify_linux_test.go +++ b/integration/run_verify_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "fmt" @@ -22,6 +22,8 @@ import ( "os/exec" "testing" + "github.com/containerd/nerdctl/cmd/nerdctl/builder" + "github.com/containerd/nerdctl/cmd/nerdctl/utils" "github.com/containerd/nerdctl/pkg/testutil" "github.com/containerd/nerdctl/pkg/testutil/testregistry" "gotest.tools/v3/assert" @@ -34,8 +36,8 @@ func TestRunVerifyCosign(t *testing.T) { testutil.DockerIncompatible(t) testutil.RequiresBuild(t) t.Setenv("COSIGN_PASSWORD", "1") - keyPair := newCosignKeyPair(t, "cosign-key-pair") - defer keyPair.cleanup() + keyPair := utils.NewCosignKeyPair(t, "cosign-key-pair") + defer keyPair.Cleanup() base := testutil.NewBase(t) defer base.Cmd("builder", "prune").Run() tID := testutil.Identifier(t) @@ -51,12 +53,12 @@ func TestRunVerifyCosign(t *testing.T) { CMD ["echo", "nerdctl-build-test-string"] `, testutil.CommonImage) - buildCtx, err := createBuildContext(dockerfile) + buildCtx, err := builder.CreateBuildContext(dockerfile) assert.NilError(t, err) defer os.RemoveAll(buildCtx) base.Cmd("build", "-t", testImageRef, buildCtx).AssertOK() - base.Cmd("push", testImageRef, "--sign=cosign", "--cosign-key="+keyPair.privateKey).AssertOK() - base.Cmd("run", "--rm", "--verify=cosign", "--cosign-key="+keyPair.publicKey, testImageRef).AssertOK() + base.Cmd("push", testImageRef, "--sign=cosign", "--cosign-key="+keyPair.PrivateKey).AssertOK() + base.Cmd("run", "--rm", "--verify=cosign", "--cosign-key="+keyPair.PublicKey, testImageRef).AssertOK() base.Cmd("run", "--rm", "--verify=cosign", "--cosign-key=dummy", testImageRef).AssertFail() } diff --git a/integration/save_linux_test.go b/integration/save_linux_test.go new file mode 100644 index 00000000000..d6848bfd650 --- /dev/null +++ b/integration/save_linux_test.go @@ -0,0 +1,46 @@ +/* + 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 integration + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/containerd/nerdctl/cmd/nerdctl/utils" + "github.com/containerd/nerdctl/pkg/testutil" + + "gotest.tools/v3/assert" +) + +func TestSave(t *testing.T) { + base := testutil.NewBase(t) + base.Cmd("pull", testutil.AlpineImage).AssertOK() + archiveTarPath := filepath.Join(t.TempDir(), "a.tar") + base.Cmd("save", "-o", archiveTarPath, testutil.AlpineImage).AssertOK() + rootfsPath := filepath.Join(t.TempDir(), "rootfs") + err := utils.ExtractDockerArchive(archiveTarPath, rootfsPath) + assert.NilError(t, err) + etcOSReleasePath := filepath.Join(rootfsPath, "/etc/os-release") + etcOSReleaseBytes, err := os.ReadFile(etcOSReleasePath) + assert.NilError(t, err) + etcOSRelease := string(etcOSReleaseBytes) + t.Logf("read %q, extracted from %q", etcOSReleasePath, testutil.AlpineImage) + t.Log(etcOSRelease) + assert.Assert(t, strings.Contains(etcOSRelease, "Alpine")) +} diff --git a/cmd/nerdctl/start_test.go b/integration/start_test.go similarity index 98% rename from cmd/nerdctl/start_test.go rename to integration/start_test.go index 1744cf52e99..7e4f8a0e98e 100644 --- a/cmd/nerdctl/start_test.go +++ b/integration/start_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "errors" diff --git a/cmd/nerdctl/stats_linux_test.go b/integration/stats_linux_test.go similarity index 98% rename from cmd/nerdctl/stats_linux_test.go rename to integration/stats_linux_test.go index 738bb855551..1e1ec8e9933 100644 --- a/cmd/nerdctl/stats_linux_test.go +++ b/integration/stats_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "fmt" diff --git a/cmd/nerdctl/stop_linux_test.go b/integration/stop_linux_test.go similarity index 99% rename from cmd/nerdctl/stop_linux_test.go rename to integration/stop_linux_test.go index a3940188ca7..60bbf0fbad6 100644 --- a/cmd/nerdctl/stop_linux_test.go +++ b/integration/stop_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "fmt" diff --git a/cmd/nerdctl/system_prune_linux_test.go b/integration/system_prune_linux_test.go similarity index 98% rename from cmd/nerdctl/system_prune_linux_test.go rename to integration/system_prune_linux_test.go index 114faeaf288..5ba3f6fe11e 100644 --- a/cmd/nerdctl/system_prune_linux_test.go +++ b/integration/system_prune_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "fmt" diff --git a/cmd/nerdctl/top_linux_test.go b/integration/top_linux_test.go similarity index 98% rename from cmd/nerdctl/top_linux_test.go rename to integration/top_linux_test.go index 52f5bb8c965..844503f9741 100644 --- a/cmd/nerdctl/top_linux_test.go +++ b/integration/top_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "testing" diff --git a/cmd/nerdctl/volume_inspect_test.go b/integration/volume_inspect_test.go similarity index 98% rename from cmd/nerdctl/volume_inspect_test.go rename to integration/volume_inspect_test.go index 6dc948b31cd..b9196e99238 100644 --- a/cmd/nerdctl/volume_inspect_test.go +++ b/integration/volume_inspect_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "crypto/rand" diff --git a/cmd/nerdctl/volume_ls_test.go b/integration/volume_ls_test.go similarity index 99% rename from cmd/nerdctl/volume_ls_test.go rename to integration/volume_ls_test.go index 9a62dd102d4..8f58e83ecee 100644 --- a/cmd/nerdctl/volume_ls_test.go +++ b/integration/volume_ls_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "errors" diff --git a/cmd/nerdctl/volume_prune_linux_test.go b/integration/volume_prune_linux_test.go similarity index 98% rename from cmd/nerdctl/volume_prune_linux_test.go rename to integration/volume_prune_linux_test.go index e9a53b064f0..69c3d7735c9 100644 --- a/cmd/nerdctl/volume_prune_linux_test.go +++ b/integration/volume_prune_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "fmt" diff --git a/cmd/nerdctl/volume_rm_linux_test.go b/integration/volume_rm_linux_test.go similarity index 98% rename from cmd/nerdctl/volume_rm_linux_test.go rename to integration/volume_rm_linux_test.go index b244a5101e2..328fb936e75 100644 --- a/cmd/nerdctl/volume_rm_linux_test.go +++ b/integration/volume_rm_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "fmt" diff --git a/cmd/nerdctl/wait_test.go b/integration/wait_test.go similarity index 98% rename from cmd/nerdctl/wait_test.go rename to integration/wait_test.go index 6a580cf143b..9bbb24ac36f 100644 --- a/cmd/nerdctl/wait_test.go +++ b/integration/wait_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package integration import ( "testing" diff --git a/pkg/testutil/testutil.go b/pkg/testutil/testutil.go index cc46c8a23c5..8cf1e1d9003 100644 --- a/pkg/testutil/testutil.go +++ b/pkg/testutil/testutil.go @@ -570,6 +570,16 @@ func RequireSystemService(t testing.TB, sv string) { } } +func RequireStargz(base *Base) { + info := base.Info() + for _, p := range info.Plugins.Storage { + if p == "stargz" { + return + } + } + base.T.Skip("test requires stargz") +} + const Namespace = "nerdctl-test" func NewBaseWithNamespace(t *testing.T, ns string) *Base {