diff options
24 files changed, 954 insertions, 53 deletions
diff --git a/cmd/podmanV2/containers/kill.go b/cmd/podmanV2/containers/kill.go new file mode 100644 index 000000000..b02dcf9da --- /dev/null +++ b/cmd/podmanV2/containers/kill.go @@ -0,0 +1,71 @@ +package containers + +import ( + "context" + "fmt" + + "github.com/containers/libpod/cmd/podmanV2/parse" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/cmd/podmanV2/utils" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/signal" + "github.com/spf13/cobra" +) + +var ( + killDescription = "The main process inside each container specified will be sent SIGKILL, or any signal specified with option --signal." + killCommand = &cobra.Command{ + Use: "kill [flags] CONTAINER [CONTAINER...]", + Short: "Kill one or more running containers with a specific signal", + Long: killDescription, + RunE: kill, + Args: func(cmd *cobra.Command, args []string) error { + return parse.CheckAllLatestAndCIDFile(cmd, args, false, false) + }, + Example: `podman kill mywebserver + podman kill 860a4b23 + podman kill --signal TERM ctrID`, + } +) + +var ( + killOptions = entities.KillOptions{} +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: killCommand, + }) + flags := killCommand.Flags() + flags.BoolVarP(&killOptions.All, "all", "a", false, "Signal all running containers") + flags.StringVarP(&killOptions.Signal, "signal", "s", "KILL", "Signal to send to the container") + flags.BoolVarP(&killOptions.Latest, "latest", "l", false, "Act on the latest container podman is aware of") + if utils.IsRemote() { + _ = flags.MarkHidden("latest") + } +} + +func kill(cmd *cobra.Command, args []string) error { + var ( + err error + errs utils.OutputErrors + ) + // Check if the signalString provided by the user is valid + // Invalid signals will return err + if _, err = signal.ParseSignalNameOrNumber(killOptions.Signal); err != nil { + return err + } + responses, err := registry.ContainerEngine().ContainerKill(context.Background(), args, killOptions) + if err != nil { + return err + } + for _, r := range responses { + if r.Err == nil { + fmt.Println(r.Id) + } else { + errs = append(errs, r.Err) + } + } + return errs.PrintErrors() +} diff --git a/cmd/podmanV2/containers/pause.go b/cmd/podmanV2/containers/pause.go new file mode 100644 index 000000000..3f3e7c38d --- /dev/null +++ b/cmd/podmanV2/containers/pause.go @@ -0,0 +1,63 @@ +package containers + +import ( + "context" + "fmt" + + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/cmd/podmanV2/utils" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/rootless" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + pauseDescription = `Pauses one or more running containers. The container name or ID can be used.` + pauseCommand = &cobra.Command{ + Use: "pause [flags] CONTAINER [CONTAINER...]", + Short: "Pause all the processes in one or more containers", + Long: pauseDescription, + RunE: pause, + Example: `podman pause mywebserver + podman pause 860a4b23 + podman pause -a`, + } + + pauseOpts = entities.PauseUnPauseOptions{} +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: pauseCommand, + }) + flags := pauseCommand.Flags() + flags.BoolVarP(&pauseOpts.All, "all", "a", false, "Pause all running containers") + pauseCommand.SetHelpTemplate(registry.HelpTemplate()) + pauseCommand.SetUsageTemplate(registry.UsageTemplate()) +} + +func pause(cmd *cobra.Command, args []string) error { + var ( + errs utils.OutputErrors + ) + if rootless.IsRootless() && !utils.IsRemote() { + return errors.New("pause is not supported for rootless containers") + } + if len(args) < 1 && !pauseOpts.All { + return errors.Errorf("you must provide at least one container name or id") + } + responses, err := registry.ContainerEngine().ContainerPause(context.Background(), args, pauseOpts) + if err != nil { + return err + } + for _, r := range responses { + if r.Err == nil { + fmt.Println(r.Id) + } else { + errs = append(errs, r.Err) + } + } + return errs.PrintErrors() +} diff --git a/cmd/podmanV2/containers/restart.go b/cmd/podmanV2/containers/restart.go new file mode 100644 index 000000000..ee9c34361 --- /dev/null +++ b/cmd/podmanV2/containers/restart.go @@ -0,0 +1,78 @@ +package containers + +import ( + "context" + "fmt" + + "github.com/containers/libpod/cmd/podmanV2/parse" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/cmd/podmanV2/utils" + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + restartDescription = `Restarts one or more running containers. The container ID or name can be used. + + A timeout before forcibly stopping can be set, but defaults to 10 seconds.` + restartCommand = &cobra.Command{ + Use: "restart [flags] CONTAINER [CONTAINER...]", + Short: "Restart one or more containers", + Long: restartDescription, + RunE: restart, + Args: func(cmd *cobra.Command, args []string) error { + return parse.CheckAllLatestAndCIDFile(cmd, args, false, false) + }, + Example: `podman restart ctrID + podman restart --latest + podman restart ctrID1 ctrID2`, + } +) + +var ( + restartOptions = entities.RestartOptions{} + restartTimeout uint +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: restartCommand, + }) + flags := restartCommand.Flags() + flags.BoolVarP(&restartOptions.All, "all", "a", false, "Restart all non-running containers") + flags.BoolVarP(&restartOptions.Latest, "latest", "l", false, "Act on the latest container podman is aware of") + flags.BoolVar(&restartOptions.Running, "running", false, "Restart only running containers when --all is used") + flags.UintVarP(&restartTimeout, "timeout", "t", define.CtrRemoveTimeout, "Seconds to wait for stop before killing the container") + flags.UintVar(&restartTimeout, "time", define.CtrRemoveTimeout, "Seconds to wait for stop before killing the container") + if utils.IsRemote() { + _ = flags.MarkHidden("latest") + } +} + +func restart(cmd *cobra.Command, args []string) error { + var ( + errs utils.OutputErrors + ) + if len(args) < 1 && !restartOptions.Latest && !restartOptions.All { + return errors.Wrapf(define.ErrInvalidArg, "you must provide at least one container name or ID") + } + + if cmd.Flag("timeout").Changed || cmd.Flag("time").Changed { + restartOptions.Timeout = &restartTimeout + } + responses, err := registry.ContainerEngine().ContainerRestart(context.Background(), args, restartOptions) + if err != nil { + return err + } + for _, r := range responses { + if r.Err == nil { + fmt.Println(r.Id) + } else { + errs = append(errs, r.Err) + } + } + return errs.PrintErrors() +} diff --git a/cmd/podmanV2/containers/rm.go b/cmd/podmanV2/containers/rm.go new file mode 100644 index 000000000..af7a38fb1 --- /dev/null +++ b/cmd/podmanV2/containers/rm.go @@ -0,0 +1,93 @@ +package containers + +import ( + "context" + "fmt" + + "github.com/containers/libpod/cmd/podmanV2/parse" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/cmd/podmanV2/utils" + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var ( + rmDescription = `Removes one or more containers from the host. The container name or ID can be used. + + Command does not remove images. Running or unusable containers will not be removed without the -f option.` + rmCommand = &cobra.Command{ + Use: "rm [flags] CONTAINER [CONTAINER...]", + Short: "Remove one or more containers", + Long: rmDescription, + RunE: rm, + Args: func(cmd *cobra.Command, args []string) error { + return parse.CheckAllLatestAndCIDFile(cmd, args, false, true) + }, + Example: `podman rm imageID + podman rm mywebserver myflaskserver 860a4b23 + podman rm --force --all + podman rm -f c684f0d469f2`, + } +) + +var ( + rmOptions = entities.RmOptions{} +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: rmCommand, + }) + flags := rmCommand.Flags() + flags.BoolVarP(&rmOptions.All, "all", "a", false, "Remove all containers") + flags.BoolVarP(&rmOptions.Ignore, "ignore", "i", false, "Ignore errors when a specified container is missing") + flags.BoolVarP(&rmOptions.Force, "force", "f", false, "Force removal of a running or unusable container. The default is false") + flags.BoolVarP(&rmOptions.Latest, "latest", "l", false, "Act on the latest container podman is aware of") + flags.BoolVar(&rmOptions.Storage, "storage", false, "Remove container from storage library") + flags.BoolVarP(&rmOptions.Volumes, "volumes", "v", false, "Remove anonymous volumes associated with the container") + flags.StringArrayVarP(&rmOptions.CIDFiles, "cidfile", "", nil, "Read the container ID from the file") + if utils.IsRemote() { + _ = flags.MarkHidden("latest") + _ = flags.MarkHidden("ignore") + _ = flags.MarkHidden("cidfile") + _ = flags.MarkHidden("storage") + } + +} + +func rm(cmd *cobra.Command, args []string) error { + var ( + errs utils.OutputErrors + ) + // Storage conflicts with --all/--latest/--volumes/--cidfile/--ignore + if rmOptions.Storage { + if rmOptions.All || rmOptions.Ignore || rmOptions.Latest || rmOptions.Volumes || rmOptions.CIDFiles != nil { + return errors.Errorf("--storage conflicts with --volumes, --all, --latest, --ignore and --cidfile") + } + } + responses, err := registry.ContainerEngine().ContainerRm(context.Background(), args, rmOptions) + if err != nil { + // TODO exitcode is a global main variable to track exit codes. + // we need this enabled + //if len(c.InputArgs) < 2 { + // exitCode = setExitCode(err) + //} + return err + } + for _, r := range responses { + if r.Err != nil { + // TODO this will not work with the remote client + if errors.Cause(err) == define.ErrWillDeadlock { + logrus.Errorf("Potential deadlock detected - please run 'podman system renumber' to resolve") + } + errs = append(errs, r.Err) + } else { + fmt.Println(r.Id) + } + } + return errs.PrintErrors() +} diff --git a/cmd/podmanV2/containers/stop.go b/cmd/podmanV2/containers/stop.go new file mode 100644 index 000000000..066202298 --- /dev/null +++ b/cmd/podmanV2/containers/stop.go @@ -0,0 +1,87 @@ +package containers + +import ( + "context" + "fmt" + + "github.com/containers/libpod/cmd/podmanV2/parse" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/cmd/podmanV2/utils" + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + stopDescription = `Stops one or more running containers. The container name or ID can be used. + + A timeout to forcibly stop the container can also be set but defaults to 10 seconds otherwise.` + stopCommand = &cobra.Command{ + Use: "stop [flags] CONTAINER [CONTAINER...]", + Short: "Stop one or more containers", + Long: stopDescription, + RunE: stop, + Args: func(cmd *cobra.Command, args []string) error { + return parse.CheckAllLatestAndCIDFile(cmd, args, false, true) + }, + Example: `podman stop ctrID + podman stop --latest + podman stop --timeout 2 mywebserver 6e534f14da9d`, + } +) + +var ( + stopOptions = entities.StopOptions{} + stopTimeout uint +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: stopCommand, + }) + flags := stopCommand.Flags() + flags.BoolVarP(&stopOptions.All, "all", "a", false, "Stop all running containers") + flags.BoolVarP(&stopOptions.Ignore, "ignore", "i", false, "Ignore errors when a specified container is missing") + flags.StringArrayVarP(&stopOptions.CIDFiles, "cidfile", "", nil, "Read the container ID from the file") + flags.BoolVarP(&stopOptions.Latest, "latest", "l", false, "Act on the latest container podman is aware of") + flags.UintVar(&stopTimeout, "time", define.CtrRemoveTimeout, "Seconds to wait for stop before killing the container") + flags.UintVarP(&stopTimeout, "timeout", "t", define.CtrRemoveTimeout, "Seconds to wait for stop before killing the container") + if registry.EngineOpts.EngineMode == entities.ABIMode { + _ = flags.MarkHidden("latest") + _ = flags.MarkHidden("cidfile") + _ = flags.MarkHidden("ignore") + } +} + +func stop(cmd *cobra.Command, args []string) error { + var ( + errs utils.OutputErrors + ) + if cmd.Flag("timeout").Changed && cmd.Flag("time").Changed { + return errors.New("the --timeout and --time flags are mutually exclusive") + } + stopOptions.Timeout = define.CtrRemoveTimeout + if cmd.Flag("timeout").Changed || cmd.Flag("time").Changed { + stopOptions.Timeout = stopTimeout + } + + // TODO How do we access global attributes? + //if c.Bool("trace") { + // span, _ := opentracing.StartSpanFromContext(Ctx, "stopCmd") + // defer span.Finish() + //} + responses, err := registry.ContainerEngine().ContainerStop(context.Background(), args, stopOptions) + if err != nil { + return err + } + for _, r := range responses { + if r.Err == nil { + fmt.Println(r.Id) + } else { + errs = append(errs, r.Err) + } + } + return errs.PrintErrors() +} diff --git a/cmd/podmanV2/containers/unpause.go b/cmd/podmanV2/containers/unpause.go new file mode 100644 index 000000000..697132a53 --- /dev/null +++ b/cmd/podmanV2/containers/unpause.go @@ -0,0 +1,60 @@ +package containers + +import ( + "context" + "fmt" + + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/cmd/podmanV2/utils" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/rootless" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + unpauseDescription = `Unpauses one or more previously paused containers. The container name or ID can be used.` + unpauseCommand = &cobra.Command{ + Use: "unpause [flags] CONTAINER [CONTAINER...]", + Short: "Unpause the processes in one or more containers", + Long: unpauseDescription, + RunE: unpause, + Example: `podman unpause ctrID + podman unpause --all`, + } + unPauseOptions = entities.PauseUnPauseOptions{} +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: unpauseCommand, + Parent: containerCmd, + }) + flags := unpauseCommand.Flags() + flags.BoolVarP(&unPauseOptions.All, "all", "a", false, "Pause all running containers") +} + +func unpause(cmd *cobra.Command, args []string) error { + var ( + errs utils.OutputErrors + ) + if rootless.IsRootless() && !utils.IsRemote() { + return errors.New("unpause is not supported for rootless containers") + } + if len(args) < 1 && !unPauseOptions.All { + return errors.Errorf("you must provide at least one container name or id") + } + responses, err := registry.ContainerEngine().ContainerUnpause(context.Background(), args, unPauseOptions) + if err != nil { + return err + } + for _, r := range responses { + if r.Err == nil { + fmt.Println(r.Id) + } else { + errs = append(errs, r.Err) + } + } + return errs.PrintErrors() +} diff --git a/cmd/podmanV2/containers/utils.go b/cmd/podmanV2/containers/utils.go new file mode 100644 index 000000000..0c09d3e40 --- /dev/null +++ b/cmd/podmanV2/containers/utils.go @@ -0,0 +1 @@ +package containers diff --git a/cmd/podmanV2/containers/wait.go b/cmd/podmanV2/containers/wait.go index 27acb3348..cded8e571 100644 --- a/cmd/podmanV2/containers/wait.go +++ b/cmd/podmanV2/containers/wait.go @@ -5,7 +5,9 @@ import ( "fmt" "time" + "github.com/containers/libpod/cmd/podmanV2/parse" "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/cmd/podmanV2/utils" "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/pkg/domain/entities" "github.com/pkg/errors" @@ -20,6 +22,9 @@ var ( Short: "Block on one or more containers", Long: waitDescription, RunE: wait, + Args: func(cmd *cobra.Command, args []string) error { + return parse.CheckAllLatestAndCIDFile(cmd, args, false, false) + }, Example: `podman wait --latest podman wait --interval 5000 ctrID podman wait ctrID1 ctrID2`, @@ -27,7 +32,7 @@ var ( ) var ( - waitFlags = entities.WaitOptions{} + waitOptions = entities.WaitOptions{} waitCondition string ) @@ -35,12 +40,11 @@ func init() { registry.Commands = append(registry.Commands, registry.CliCommand{ Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, Command: waitCommand, - Parent: containerCmd, }) flags := waitCommand.Flags() - flags.DurationVarP(&waitFlags.Interval, "interval", "i", time.Duration(250), "Milliseconds to wait before polling for completion") - flags.BoolVarP(&waitFlags.Latest, "latest", "l", false, "Act on the latest container podman is aware of") + flags.DurationVarP(&waitOptions.Interval, "interval", "i", time.Duration(250), "Milliseconds to wait before polling for completion") + flags.BoolVarP(&waitOptions.Latest, "latest", "l", false, "Act on the latest container podman is aware of") flags.StringVar(&waitCondition, "condition", "stopped", "Condition to wait on") if registry.EngineOpts.EngineMode == entities.ABIMode { // TODO: This is the same as V1. We could skip creating the flag altogether in V2... @@ -50,33 +54,28 @@ func init() { func wait(cmd *cobra.Command, args []string) error { var ( - err error + err error + errs utils.OutputErrors ) - if waitFlags.Latest && len(args) > 0 { - return errors.New("cannot combine latest flag and arguments") - } - if waitFlags.Interval == 0 { + if waitOptions.Interval == 0 { return errors.New("interval must be greater then 0") } - waitFlags.Condition, err = define.StringToContainerStatus(waitCondition) + waitOptions.Condition, err = define.StringToContainerStatus(waitCondition) if err != nil { return err } - responses, err := registry.ContainerEngine().ContainerWait(context.Background(), args, waitFlags) + responses, err := registry.ContainerEngine().ContainerWait(context.Background(), args, waitOptions) if err != nil { return err } for _, r := range responses { if r.Error == nil { fmt.Println(r.Id) + } else { + errs = append(errs, r.Error) } } - for _, r := range responses { - if r.Error != nil { - fmt.Println(err) - } - } - return nil + return errs.PrintErrors() } diff --git a/cmd/podmanV2/parse/parse.go b/cmd/podmanV2/parse/parse.go index 03cda268c..10d2146fa 100644 --- a/cmd/podmanV2/parse/parse.go +++ b/cmd/podmanV2/parse/parse.go @@ -13,6 +13,7 @@ import ( "strings" "github.com/pkg/errors" + "github.com/spf13/cobra" ) const ( @@ -186,3 +187,47 @@ func ValidURL(urlStr string) error { } return nil } + +// checkAllLatestAndCIDFile checks that --all and --latest are used correctly. +// If cidfile is set, also check for the --cidfile flag. +func CheckAllLatestAndCIDFile(c *cobra.Command, args []string, ignoreArgLen bool, cidfile bool) error { + argLen := len(args) + if c.Flags().Lookup("all") == nil || c.Flags().Lookup("latest") == nil { + if !cidfile { + return errors.New("unable to lookup values for 'latest' or 'all'") + } else if c.Flags().Lookup("cidfile") == nil { + return errors.New("unable to lookup values for 'latest', 'all' or 'cidfile'") + } + } + + specifiedAll, _ := c.Flags().GetBool("all") + specifiedLatest, _ := c.Flags().GetBool("latest") + specifiedCIDFile := false + if cid, _ := c.Flags().GetStringArray("cidfile"); len(cid) > 0 { + specifiedCIDFile = true + } + + if specifiedCIDFile && (specifiedAll || specifiedLatest) { + return errors.Errorf("--all, --latest and --cidfile cannot be used together") + } else if specifiedAll && specifiedLatest { + return errors.Errorf("--all and --latest cannot be used together") + } + + if ignoreArgLen { + return nil + } + if (argLen > 0) && (specifiedAll || specifiedLatest) { + return errors.Errorf("no arguments are needed with --all or --latest") + } else if cidfile && (argLen > 0) && (specifiedAll || specifiedLatest || specifiedCIDFile) { + return errors.Errorf("no arguments are needed with --all, --latest or --cidfile") + } + + if specifiedCIDFile { + return nil + } + + if argLen < 1 && !specifiedAll && !specifiedLatest && !specifiedCIDFile { + return errors.Errorf("you must provide at least one name or id") + } + return nil +} diff --git a/cmd/podmanV2/root.go b/cmd/podmanV2/root.go index 24b083b9f..b0dd7643f 100644 --- a/cmd/podmanV2/root.go +++ b/cmd/podmanV2/root.go @@ -23,8 +23,9 @@ var rootCmd = &cobra.Command{ func init() { // Override default --help information of `--version` global flag} var dummyVersion bool - rootCmd.PersistentFlags().BoolVarP(&dummyVersion, "version", "v", false, "Version of podman") - rootCmd.PersistentFlags().StringVarP(®istry.EngineOpts.Uri, "remote", "r", "", "URL to access podman service") + // TODO had to disable shorthand -v for version due to -v rm with volume + rootCmd.PersistentFlags().BoolVar(&dummyVersion, "version", false, "Version of Podman") + rootCmd.PersistentFlags().StringVarP(®istry.EngineOpts.Uri, "remote", "r", "", "URL to access Podman service") rootCmd.PersistentFlags().StringSliceVar(®istry.EngineOpts.Identities, "identity", []string{}, "path to SSH identity file") } diff --git a/cmd/podmanV2/utils/error.go b/cmd/podmanV2/utils/error.go new file mode 100644 index 000000000..3464f0779 --- /dev/null +++ b/cmd/podmanV2/utils/error.go @@ -0,0 +1,16 @@ +package utils + +import "fmt" + +type OutputErrors []error + +func (o OutputErrors) PrintErrors() (lastError error) { + if len(o) == 0 { + return + } + lastError = o[len(o)-1] + for e := 0; e < len(o)-1; e++ { + fmt.Println(o[e]) + } + return +} diff --git a/cmd/podmanV2/utils/remote.go b/cmd/podmanV2/utils/remote.go new file mode 100644 index 000000000..d0c8a272d --- /dev/null +++ b/cmd/podmanV2/utils/remote.go @@ -0,0 +1,10 @@ +package utils + +import ( + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" +) + +func IsRemote() bool { + return registry.EngineOpts.EngineMode == entities.TunnelMode +} diff --git a/pkg/api/handlers/compat/containers.go b/pkg/api/handlers/compat/containers.go index 3a269fe50..2ce113d30 100644 --- a/pkg/api/handlers/compat/containers.go +++ b/pkg/api/handlers/compat/containers.go @@ -7,7 +7,6 @@ import ( "strconv" "strings" "sync" - "syscall" "time" "github.com/containers/libpod/libpod" @@ -15,6 +14,7 @@ import ( "github.com/containers/libpod/libpod/logs" "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/containers/libpod/pkg/signal" "github.com/containers/libpod/pkg/util" "github.com/gorilla/schema" "github.com/pkg/errors" @@ -145,14 +145,20 @@ func KillContainer(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value("runtime").(*libpod.Runtime) decoder := r.Context().Value("decoder").(*schema.Decoder) query := struct { - Signal syscall.Signal `schema:"signal"` + Signal string `schema:"signal"` }{ - Signal: syscall.SIGKILL, + Signal: "KILL", } if err := decoder.Decode(&query, r.URL.Query()); err != nil { utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) return } + + sig, err := signal.ParseSignalNameOrNumber(query.Signal) + if err != nil { + utils.InternalServerError(w, err) + return + } name := utils.GetName(r) con, err := runtime.LookupContainer(name) if err != nil { @@ -172,7 +178,7 @@ func KillContainer(w http.ResponseWriter, r *http.Request) { return } - err = con.Kill(uint(query.Signal)) + err = con.Kill(uint(sig)) if err != nil { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrapf(err, "unable to kill Container %s", name)) } diff --git a/pkg/api/handlers/compat/containers_restart.go b/pkg/api/handlers/compat/containers_restart.go index 5b8fafaa4..343bf96d2 100644 --- a/pkg/api/handlers/compat/containers_restart.go +++ b/pkg/api/handlers/compat/containers_restart.go @@ -1,11 +1,9 @@ package compat import ( - "fmt" "net/http" "github.com/containers/libpod/libpod" - "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/pkg/api/handlers/utils" "github.com/gorilla/schema" "github.com/pkg/errors" @@ -32,20 +30,6 @@ func RestartContainer(w http.ResponseWriter, r *http.Request) { return } - state, err := con.State() - if err != nil { - utils.InternalServerError(w, err) - return - } - - // FIXME: This is not in the swagger.yml... - // If the Container is stopped already, send a 409 - if state == define.ContainerStateStopped || state == define.ContainerStateExited { - msg := fmt.Sprintf("Container %s is not running", name) - utils.Error(w, msg, http.StatusConflict, errors.New(msg)) - return - } - timeout := con.StopTimeout() if _, found := r.URL.Query()["t"]; found { timeout = uint(query.Timeout) diff --git a/pkg/bindings/containers/containers.go b/pkg/bindings/containers/containers.go index 534555a00..231b6f232 100644 --- a/pkg/bindings/containers/containers.go +++ b/pkg/bindings/containers/containers.go @@ -126,13 +126,13 @@ func Inspect(ctx context.Context, nameOrID string, size *bool) (*libpod.InspectC // Kill sends a given signal to a given container. The signal should be the string // representation of a signal like 'SIGKILL'. The nameOrID can be a container name // or a partial/full ID -func Kill(ctx context.Context, nameOrID string, signal string) error { +func Kill(ctx context.Context, nameOrID string, sig string) error { conn, err := bindings.GetClient(ctx) if err != nil { return err } params := url.Values{} - params.Set("signal", signal) + params.Set("signal", sig) response, err := conn.DoRequest(nil, http.MethodPost, "/containers/%s/kill", params, nameOrID) if err != nil { return err @@ -247,14 +247,14 @@ func Exists(ctx context.Context, nameOrID string) (bool, error) { // Stop stops a running container. The timeout is optional. The nameOrID can be a container name // or a partial/full ID -func Stop(ctx context.Context, nameOrID string, timeout *int) error { +func Stop(ctx context.Context, nameOrID string, timeout *uint) error { params := url.Values{} conn, err := bindings.GetClient(ctx) if err != nil { return err } if timeout != nil { - params.Set("t", strconv.Itoa(*timeout)) + params.Set("t", strconv.Itoa(int(*timeout))) } response, err := conn.DoRequest(nil, http.MethodPost, "/containers/%s/stop", params, nameOrID) if err != nil { diff --git a/pkg/bindings/test/volumes_test.go b/pkg/bindings/test/volumes_test.go index 9da034d24..59fe48f22 100644 --- a/pkg/bindings/test/volumes_test.go +++ b/pkg/bindings/test/volumes_test.go @@ -102,7 +102,7 @@ var _ = Describe("Podman volumes", func() { Expect(code).To(BeNumerically("==", http.StatusConflict)) // Removing with a volume in use with force should work with a stopped container - zero := 0 + zero := uint(0) err = containers.Stop(connText, "vtest", &zero) Expect(err).To(BeNil()) err = volumes.Remove(connText, vol.Name, &bindings.PTrue) diff --git a/pkg/domain/entities/containers.go b/pkg/domain/entities/containers.go index 0e1208b3b..8b7406ae8 100644 --- a/pkg/domain/entities/containers.go +++ b/pkg/domain/entities/containers.go @@ -21,3 +21,63 @@ type WaitReport struct { type BoolReport struct { Value bool } + +type PauseUnPauseOptions struct { + All bool +} + +type PauseUnpauseReport struct { + Err error + Id string +} + +type StopOptions struct { + All bool + CIDFiles []string + Ignore bool + Latest bool + Timeout uint +} + +type StopReport struct { + Err error + Id string +} + +type KillOptions struct { + All bool + Latest bool + Signal string +} + +type KillReport struct { + Err error + Id string +} + +type RestartOptions struct { + All bool + Latest bool + Running bool + Timeout *uint +} + +type RestartReport struct { + Err error + Id string +} + +type RmOptions struct { + All bool + CIDFiles []string + Force bool + Ignore bool + Latest bool + Storage bool + Volumes bool +} + +type RmReport struct { + Err error + Id string +} diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go index 5820c12c3..2887f0b51 100644 --- a/pkg/domain/entities/engine_container.go +++ b/pkg/domain/entities/engine_container.go @@ -5,9 +5,14 @@ import ( ) type ContainerEngine interface { - ContainerDelete(ctx context.Context, opts ContainerDeleteOptions) (*ContainerDeleteReport, error) ContainerPrune(ctx context.Context) (*ContainerPruneReport, error) ContainerExists(ctx context.Context, nameOrId string) (*BoolReport, error) + ContainerKill(ctx context.Context, namesOrIds []string, options KillOptions) ([]*KillReport, error) + ContainerPause(ctx context.Context, namesOrIds []string, options PauseUnPauseOptions) ([]*PauseUnpauseReport, error) + ContainerRestart(ctx context.Context, namesOrIds []string, options RestartOptions) ([]*RestartReport, error) + ContainerRm(ctx context.Context, namesOrIds []string, options RmOptions) ([]*RmReport, error) + ContainerUnpause(ctx context.Context, namesOrIds []string, options PauseUnPauseOptions) ([]*PauseUnpauseReport, error) + ContainerStop(ctx context.Context, namesOrIds []string, options StopOptions) ([]*StopReport, error) ContainerWait(ctx context.Context, namesOrIds []string, options WaitOptions) ([]WaitReport, error) PodDelete(ctx context.Context, opts PodPruneOptions) (*PodDeleteReport, error) PodExists(ctx context.Context, nameOrId string) (*BoolReport, error) diff --git a/pkg/domain/infra/abi/containers.go b/pkg/domain/infra/abi/containers.go index cdcd77246..47deb010a 100644 --- a/pkg/domain/infra/abi/containers.go +++ b/pkg/domain/infra/abi/containers.go @@ -4,11 +4,16 @@ package abi import ( "context" + "io/ioutil" + "strings" + "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/pkg/adapter/shortcuts" "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/signal" "github.com/pkg/errors" + "github.com/sirupsen/logrus" ) // TODO: Should return *entities.ContainerExistsReport, error @@ -41,8 +46,198 @@ func (ic *ContainerEngine) ContainerWait(ctx context.Context, namesOrIds []strin return responses, nil } -func (ic *ContainerEngine) ContainerDelete(ctx context.Context, opts entities.ContainerDeleteOptions) (*entities.ContainerDeleteReport, error) { - panic("implement me") +func (ic *ContainerEngine) ContainerPause(ctx context.Context, namesOrIds []string, options entities.PauseUnPauseOptions) ([]*entities.PauseUnpauseReport, error) { + var ( + ctrs []*libpod.Container + err error + report []*entities.PauseUnpauseReport + ) + if options.All { + ctrs, err = ic.Libpod.GetAllContainers() + } else { + ctrs, err = shortcuts.GetContainersByContext(false, false, namesOrIds, ic.Libpod) + } + if err != nil { + return nil, err + } + for _, c := range ctrs { + err := c.Pause() + report = append(report, &entities.PauseUnpauseReport{Id: c.ID(), Err: err}) + } + return report, nil +} + +func (ic *ContainerEngine) ContainerUnpause(ctx context.Context, namesOrIds []string, options entities.PauseUnPauseOptions) ([]*entities.PauseUnpauseReport, error) { + var ( + ctrs []*libpod.Container + err error + report []*entities.PauseUnpauseReport + ) + if options.All { + ctrs, err = ic.Libpod.GetAllContainers() + } else { + ctrs, err = shortcuts.GetContainersByContext(false, false, namesOrIds, ic.Libpod) + } + if err != nil { + return nil, err + } + for _, c := range ctrs { + err := c.Unpause() + report = append(report, &entities.PauseUnpauseReport{Id: c.ID(), Err: err}) + } + return report, nil +} +func (ic *ContainerEngine) ContainerStop(ctx context.Context, namesOrIds []string, options entities.StopOptions) ([]*entities.StopReport, error) { + var ( + reports []*entities.StopReport + ) + names := namesOrIds + for _, cidFile := range options.CIDFiles { + content, err := ioutil.ReadFile(cidFile) + if err != nil { + return nil, errors.Wrap(err, "error reading CIDFile") + } + id := strings.Split(string(content), "\n")[0] + names = append(names, id) + } + ctrs, err := shortcuts.GetContainersByContext(options.All, options.Latest, names, ic.Libpod) + if err != nil && !(options.Ignore && errors.Cause(err) == define.ErrNoSuchCtr) { + return nil, err + } + for _, con := range ctrs { + report := entities.StopReport{Id: con.ID()} + err = con.StopWithTimeout(options.Timeout) + if err != nil { + // These first two are considered non-fatal under the right conditions + if errors.Cause(err) == define.ErrCtrStopped { + logrus.Debugf("Container %s is already stopped", con.ID()) + reports = append(reports, &report) + continue + + } else if options.All && errors.Cause(err) == define.ErrCtrStateInvalid { + logrus.Debugf("Container %s is not running, could not stop", con.ID()) + reports = append(reports, &report) + continue + } + report.Err = err + reports = append(reports, &report) + continue + } + reports = append(reports, &report) + } + return reports, nil +} + +func (ic *ContainerEngine) ContainerKill(ctx context.Context, namesOrIds []string, options entities.KillOptions) ([]*entities.KillReport, error) { + var ( + reports []*entities.KillReport + ) + sig, err := signal.ParseSignalNameOrNumber(options.Signal) + if err != nil { + return nil, err + } + ctrs, err := shortcuts.GetContainersByContext(options.All, options.Latest, namesOrIds, ic.Libpod) + if err != nil { + return nil, err + } + for _, con := range ctrs { + reports = append(reports, &entities.KillReport{ + Id: con.ID(), + Err: con.Kill(uint(sig)), + }) + } + return reports, nil +} +func (ic *ContainerEngine) ContainerRestart(ctx context.Context, namesOrIds []string, options entities.RestartOptions) ([]*entities.RestartReport, error) { + var ( + reports []*entities.RestartReport + ) + ctrs, err := shortcuts.GetContainersByContext(options.All, options.Latest, namesOrIds, ic.Libpod) + if err != nil { + return nil, err + } + for _, con := range ctrs { + timeout := con.StopTimeout() + if options.Timeout != nil { + timeout = *options.Timeout + } + reports = append(reports, &entities.RestartReport{ + Id: con.ID(), + Err: con.RestartWithTimeout(ctx, timeout), + }) + } + return reports, nil +} + +func (ic *ContainerEngine) ContainerRm(ctx context.Context, namesOrIds []string, options entities.RmOptions) ([]*entities.RmReport, error) { + var ( + reports []*entities.RmReport + ) + if options.Storage { + for _, ctr := range namesOrIds { + report := entities.RmReport{Id: ctr} + if err := ic.Libpod.RemoveStorageContainer(ctr, options.Force); err != nil { + report.Err = err + } + reports = append(reports, &report) + } + return reports, nil + } + + names := namesOrIds + for _, cidFile := range options.CIDFiles { + content, err := ioutil.ReadFile(cidFile) + if err != nil { + return nil, errors.Wrap(err, "error reading CIDFile") + } + id := strings.Split(string(content), "\n")[0] + names = append(names, id) + } + + ctrs, err := shortcuts.GetContainersByContext(options.All, options.Latest, names, ic.Libpod) + if err != nil && !(options.Ignore && errors.Cause(err) == define.ErrNoSuchCtr) { + // Failed to get containers. If force is specified, get the containers ID + // and evict them + if !options.Force { + return nil, err + } + + for _, ctr := range namesOrIds { + logrus.Debugf("Evicting container %q", ctr) + report := entities.RmReport{Id: ctr} + id, err := ic.Libpod.EvictContainer(ctx, ctr, options.Volumes) + if err != nil { + if options.Ignore && errors.Cause(err) == define.ErrNoSuchCtr { + logrus.Debugf("Ignoring error (--allow-missing): %v", err) + reports = append(reports, &report) + continue + } + report.Err = errors.Wrapf(err, "Failed to evict container: %q", id) + reports = append(reports, &report) + continue + } + reports = append(reports, &report) + } + return reports, nil + } + + for _, c := range ctrs { + report := entities.RmReport{Id: c.ID()} + err := ic.Libpod.RemoveContainer(ctx, c, options.Force, options.Volumes) + if err != nil { + if options.Ignore && errors.Cause(err) == define.ErrNoSuchCtr { + logrus.Debugf("Ignoring error (--allow-missing): %v", err) + reports = append(reports, &report) + continue + } + logrus.Debugf("Failed to remove container %s: %s", c.ID(), err.Error()) + report.Err = err + reports = append(reports, &report) + continue + } + reports = append(reports, &report) + } + return reports, nil } func (ic *ContainerEngine) ContainerPrune(ctx context.Context) (*entities.ContainerPruneReport, error) { diff --git a/pkg/domain/infra/tunnel/containers.go b/pkg/domain/infra/tunnel/containers.go index 8bf74126d..21f62df6b 100644 --- a/pkg/domain/infra/tunnel/containers.go +++ b/pkg/domain/infra/tunnel/containers.go @@ -40,3 +40,108 @@ func (r *ContainerEngine) ContainerDelete(ctx context.Context, opts entities.Con func (r *ContainerEngine) ContainerPrune(ctx context.Context) (*entities.ContainerPruneReport, error) { panic("implement me") } +func (ic *ContainerEngine) ContainerPause(ctx context.Context, namesOrIds []string, options entities.PauseUnPauseOptions) ([]*entities.PauseUnpauseReport, error) { + var ( + reports []*entities.PauseUnpauseReport + ) + ctrs, err := getContainersByContext(ic.ClientCxt, options.All, namesOrIds) + if err != nil { + return nil, err + } + for _, c := range ctrs { + err := containers.Pause(ic.ClientCxt, c.ID) + reports = append(reports, &entities.PauseUnpauseReport{Id: c.ID, Err: err}) + } + return reports, nil +} + +func (ic *ContainerEngine) ContainerUnpause(ctx context.Context, namesOrIds []string, options entities.PauseUnPauseOptions) ([]*entities.PauseUnpauseReport, error) { + var ( + reports []*entities.PauseUnpauseReport + ) + ctrs, err := getContainersByContext(ic.ClientCxt, options.All, namesOrIds) + if err != nil { + return nil, err + } + for _, c := range ctrs { + err := containers.Unpause(ic.ClientCxt, c.ID) + reports = append(reports, &entities.PauseUnpauseReport{Id: c.ID, Err: err}) + } + return reports, nil +} + +func (ic *ContainerEngine) ContainerStop(ctx context.Context, namesOrIds []string, options entities.StopOptions) ([]*entities.StopReport, error) { + var ( + reports []*entities.StopReport + ) + ctrs, err := getContainersByContext(ic.ClientCxt, options.All, namesOrIds) + if err != nil { + return nil, err + } + for _, c := range ctrs { + report := entities.StopReport{Id: c.ID} + report.Err = containers.Stop(ic.ClientCxt, c.ID, &options.Timeout) + // TODO we need to associate errors returned by http with common + // define.errors so that we can equity tests. this will allow output + // to be the same as the native client + reports = append(reports, &report) + } + return reports, nil +} + +func (ic *ContainerEngine) ContainerKill(ctx context.Context, namesOrIds []string, options entities.KillOptions) ([]*entities.KillReport, error) { + var ( + reports []*entities.KillReport + ) + ctrs, err := getContainersByContext(ic.ClientCxt, options.All, namesOrIds) + if err != nil { + return nil, err + } + for _, c := range ctrs { + reports = append(reports, &entities.KillReport{ + Id: c.ID, + Err: containers.Kill(ic.ClientCxt, c.ID, options.Signal), + }) + } + return reports, nil +} + +func (ic *ContainerEngine) ContainerRestart(ctx context.Context, namesOrIds []string, options entities.RestartOptions) ([]*entities.RestartReport, error) { + var ( + reports []*entities.RestartReport + timeout *int + ) + if options.Timeout != nil { + t := int(*options.Timeout) + timeout = &t + } + ctrs, err := getContainersByContext(ic.ClientCxt, options.All, namesOrIds) + if err != nil { + return nil, err + } + for _, c := range ctrs { + reports = append(reports, &entities.RestartReport{ + Id: c.ID, + Err: containers.Restart(ic.ClientCxt, c.ID, timeout), + }) + } + return reports, nil +} + +func (ic *ContainerEngine) ContainerRm(ctx context.Context, namesOrIds []string, options entities.RmOptions) ([]*entities.RmReport, error) { + var ( + reports []*entities.RmReport + ) + ctrs, err := getContainersByContext(ic.ClientCxt, options.All, namesOrIds) + if err != nil { + return nil, err + } + // TODO there is no endpoint for container eviction. Need to discuss + for _, c := range ctrs { + reports = append(reports, &entities.RmReport{ + Id: c.ID, + Err: containers.Remove(ic.ClientCxt, c.ID, &options.Force, &options.Volumes), + }) + } + return reports, nil +} diff --git a/pkg/domain/infra/tunnel/helpers.go b/pkg/domain/infra/tunnel/helpers.go index d5a3224c2..11fca5278 100644 --- a/pkg/domain/infra/tunnel/helpers.go +++ b/pkg/domain/infra/tunnel/helpers.go @@ -2,6 +2,7 @@ package tunnel import ( "context" + "strings" "github.com/containers/libpod/pkg/api/handlers/libpod" "github.com/containers/libpod/pkg/bindings" @@ -27,7 +28,7 @@ func getContainersByContext(contextWithConnection context.Context, all bool, nam for _, id := range namesOrIds { var found bool for _, con := range c { - if id == con.ID || util.StringInSlice(id, con.Names) { + if id == con.ID || strings.HasPrefix(con.ID, id) || util.StringInSlice(id, con.Names) { cons = append(cons, con) found = true break diff --git a/pkg/signal/signal_linux.go b/pkg/signal/signal_linux.go index 3d549898f..76ab16ec7 100644 --- a/pkg/signal/signal_linux.go +++ b/pkg/signal/signal_linux.go @@ -104,11 +104,11 @@ func ParseSignal(rawSignal string) (syscall.Signal, error) { } return syscall.Signal(s), nil } - signal, ok := signalMap[strings.TrimPrefix(strings.ToUpper(rawSignal), "SIG")] + sig, ok := signalMap[strings.TrimPrefix(strings.ToUpper(rawSignal), "SIG")] if !ok { return -1, fmt.Errorf("invalid signal: %s", rawSignal) } - return signal, nil + return sig, nil } // CatchAll catches all signals and relays them to the specified channel. @@ -125,3 +125,18 @@ func StopCatch(sigc chan os.Signal) { signal.Stop(sigc) close(sigc) } + +// ParseSignalNameOrNumber translates a string to a valid syscall signal. Input +// can be a name or number representation i.e. "KILL" "9" +func ParseSignalNameOrNumber(rawSignal string) (syscall.Signal, error) { + s, err := ParseSignal(rawSignal) + if err == nil { + return s, nil + } + for k, v := range signalMap { + if k == strings.ToUpper(rawSignal) { + return v, nil + } + } + return -1, fmt.Errorf("invalid signal: %s", rawSignal) +} diff --git a/pkg/signal/signal_unsupported.go b/pkg/signal/signal_unsupported.go index 0a92a5b3a..f946d802d 100644 --- a/pkg/signal/signal_unsupported.go +++ b/pkg/signal/signal_unsupported.go @@ -26,3 +26,9 @@ func CatchAll(sigc chan os.Signal) { func StopCatch(sigc chan os.Signal) { panic("Unsupported on non-linux platforms") } + +// ParseSignalNameOrNumber translates a string to a valid syscall signal. Input +// can be a name or number representation i.e. "KILL" "9" +func ParseSignalNameOrNumber(rawSignal string) (syscall.Signal, error) { + return 0, fmt.Errorf("unsupported on non-linux platforms") +} diff --git a/pkg/util/utils.go b/pkg/util/utils.go index a4df48c88..3e11c010a 100644 --- a/pkg/util/utils.go +++ b/pkg/util/utils.go @@ -309,15 +309,15 @@ func ParseSignal(rawSignal string) (syscall.Signal, error) { // Strip off leading dash, to allow -1 or -HUP basename := strings.TrimPrefix(rawSignal, "-") - signal, err := signal.ParseSignal(basename) + sig, err := signal.ParseSignal(basename) if err != nil { return -1, err } // 64 is SIGRTMAX; wish we could get this from a standard Go library - if signal < 1 || signal > 64 { + if sig < 1 || sig > 64 { return -1, errors.Errorf("valid signals are 1 through 64") } - return signal, nil + return sig, nil } // ParseIDMapping takes idmappings and subuid and subgid maps and returns a storage mapping |