diff options
author | baude <bbaude@redhat.com> | 2020-01-16 11:23:16 -0600 |
---|---|---|
committer | Brent Baude <bbaude@redhat.com> | 2020-01-21 16:35:45 -0600 |
commit | d4c2aaf38ad066e742dad530535faade39dadd1a (patch) | |
tree | 062e1f1f8499375c5124487769e13b7b5fb788f1 | |
parent | f63005e0f23ae6a71c2c910014c5e19623272f73 (diff) | |
download | podman-d4c2aaf38ad066e742dad530535faade39dadd1a.tar.gz podman-d4c2aaf38ad066e742dad530535faade39dadd1a.tar.bz2 podman-d4c2aaf38ad066e742dad530535faade39dadd1a.zip |
Add service endpoint
add service endpoint for the new API. Also supports the varlink
implementation.
Signed-off-by: baude <bbaude@redhat.com>
Refactor to allow developer more control of API server
* Add api.NewServerWithSettings() to create an API server with custom
settings
* Add api.ListenUnix() to create a UDS net.Listener and setup UDS
Signed-off-by: Jhon Honce <jhonce@redhat.com>
Signed-off-by: baude <bbaude@redhat.com>
More service completion
Add podman service command that allows users to run either a RESTful or
varlink protocol API service.
Addition of docs and RESTful listening.
Signed-off-by: baude <bbaude@redhat.com>
Signed-off-by: Brent Baude <bbaude@redhat.com>
-rw-r--r-- | .cirrus.yml | 47 | ||||
-rw-r--r-- | Makefile | 10 | ||||
-rw-r--r-- | cmd/podman/cliconfig/config.go | 6 | ||||
-rw-r--r-- | cmd/podman/commands.go | 3 | ||||
-rw-r--r-- | cmd/podman/service.go | 154 | ||||
-rw-r--r-- | cmd/podman/service_dummy.go | 11 | ||||
-rw-r--r-- | cmd/podman/varlink.go | 2 | ||||
-rw-r--r-- | cmd/service/main.go | 55 | ||||
-rw-r--r-- | commands.md | 1 | ||||
-rw-r--r-- | completions/bash/podman | 13 | ||||
-rw-r--r-- | docs/source/markdown/podman-service.1.md | 47 | ||||
-rw-r--r-- | docs/source/markdown/podman.1.md | 1 | ||||
-rw-r--r-- | pkg/adapter/client.go | 2 | ||||
-rw-r--r-- | pkg/adapter/client_config.go | 7 | ||||
-rw-r--r-- | pkg/api/server/listener_api.go | 31 | ||||
-rw-r--r-- | pkg/api/server/server.go | 50 | ||||
-rw-r--r-- | pkg/bindings/containers.go | 8 |
17 files changed, 341 insertions, 107 deletions
diff --git a/.cirrus.yml b/.cirrus.yml index 341b7549e..e1810fab6 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -640,29 +640,29 @@ verify_test_built_images_task: always: <<: *standardlogs -upload_snap_task: - only_if: >- - $CIRRUS_BRANCH != $DEST_BRANCH && - $CIRRUS_CHANGE_MESSAGE !=~ '.*CI:IMG.*' && - $CIRRUS_CHANGE_MESSAGE !=~ '.*CI:DOCS.*' - - # Only when PR or branch is merged into master - - depends_on: - - "test_building_snap" - - container: - image: yakshaveinc/snapcraft:core18 - - env: - SNAPCRAFT_LOGIN: ENCRYPTED[d8e82eb31c6372fec07f405f413d57806026b1a9f8400033531ebcd54d6750a5e4a8b1f68e3ec65c98c65e0d9b2a6a75] - snapcraft_login_file: - path: /root/.snapcraft/login.cfg - variable_name: SNAPCRAFT_LOGIN - snapcraft_script: - - 'apt-get -y update' - - 'snapcraft login --with "/root/.snapcraft/login.cfg"' - - 'cd contrib/snapcraft && snapcraft && snapcraft push *.snap --release edge' + #upload_snap_task: + # only_if: >- + # $CIRRUS_BRANCH != $DEST_BRANCH && + # $CIRRUS_CHANGE_MESSAGE !=~ '.*CI:IMG.*' && + # $CIRRUS_CHANGE_MESSAGE !=~ '.*CI:DOCS.*' + # + # # Only when PR or branch is merged into master + # + # depends_on: + # - "test_building_snap" + # + # container: + # image: yakshaveinc/snapcraft:core18 + # + # env: + # SNAPCRAFT_LOGIN: ENCRYPTED[d8e82eb31c6372fec07f405f413d57806026b1a9f8400033531ebcd54d6750a5e4a8b1f68e3ec65c98c65e0d9b2a6a75] + # snapcraft_login_file: + # path: /root/.snapcraft/login.cfg + # variable_name: SNAPCRAFT_LOGIN + # snapcraft_script: + # - 'apt-get -y update' + # - 'snapcraft login --with "/root/.snapcraft/login.cfg"' + # - 'cd contrib/snapcraft && snapcraft && snapcraft push *.snap --release edge' docs_task: @@ -705,7 +705,6 @@ success_task: - "special_testing_endpoint" - "test_build_cache_images" - "test_building_snap" - - "upload_snap" - "verify_test_built_images" - "docs" @@ -199,14 +199,6 @@ bin/podman.cross.%: .gopathok GOARCH="$${TARGET##*.}" \ $(GO_BUILD) -gcflags '$(GCFLAGS)' -asmflags '$(ASMFLAGS)' -ldflags '$(LDFLAGS_PODMAN)' -tags '$(BUILDTAGS_CROSS)' -o "$@" $(PROJECT)/cmd/podman -.PHONY: service -service: .gopathok - $(GO_BUILD) $(BUILDFLAGS) -gcflags '$(GCFLAGS)' -asmflags '$(ASMFLAGS)' -ldflags '$(LDFLAGS_PODMAN)' -tags "$(BUILDTAGS)" -o bin/$@ $(PROJECT)/cmd/service - -.PHONY: -run-service: - systemd-socket-activate -l 8080 ./bin/service - .PHONY: run-docker-py-tests run-docker-py-tests: $(eval testLogs=$(shell mktemp)) @@ -328,7 +320,7 @@ system.test-binary: .install.ginkgo vagrant-check: BOX=$(BOX) sh ./vagrant.sh -binaries: varlink_generate podman podman-remote service ## Build podman +binaries: varlink_generate podman podman-remote ## Build podman install.catatonit: ./hack/install_catatonit.sh diff --git a/cmd/podman/cliconfig/config.go b/cmd/podman/cliconfig/config.go index b261599e6..6bc8aa4a3 100644 --- a/cmd/podman/cliconfig/config.go +++ b/cmd/podman/cliconfig/config.go @@ -599,6 +599,12 @@ type VarlinkValues struct { Timeout int64 } +type ServiceValues struct { + PodmanCommand + Varlink bool + Timeout int64 +} + type SetTrustValues struct { PodmanCommand PolicyPath string diff --git a/cmd/podman/commands.go b/cmd/podman/commands.go index 31f1b3ba4..ebd7aeb0c 100644 --- a/cmd/podman/commands.go +++ b/cmd/podman/commands.go @@ -26,6 +26,9 @@ func getMainCommands() []*cobra.Command { if len(_varlinkCommand.Use) > 0 { rootCommands = append(rootCommands, _varlinkCommand) } + if len(_serviceCommand.Use) > 0 { + rootCommands = append(rootCommands, _serviceCommand) + } return rootCommands } diff --git a/cmd/podman/service.go b/cmd/podman/service.go new file mode 100644 index 000000000..6e2b4a366 --- /dev/null +++ b/cmd/podman/service.go @@ -0,0 +1,154 @@ +// +build varlink,!remoteclient + +package main + +import ( + "fmt" + "net" + "os" + "path/filepath" + "strings" + "time" + + "github.com/containers/libpod/cmd/podman/cliconfig" + "github.com/containers/libpod/cmd/podman/libpodruntime" + iopodman "github.com/containers/libpod/cmd/podman/varlink" + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/adapter" + api "github.com/containers/libpod/pkg/api/server" + "github.com/containers/libpod/pkg/rootless" + "github.com/containers/libpod/pkg/util" + "github.com/containers/libpod/pkg/varlinkapi" + "github.com/containers/libpod/version" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/varlink/go/varlink" +) + +var ( + serviceCommand cliconfig.ServiceValues + serviceDescription = `Run an API service + +Enable a listening service for API access to Podman commands. +` + + _serviceCommand = &cobra.Command{ + Use: "service [flags] [URI]", + Short: "Run API service", + Long: serviceDescription, + RunE: func(cmd *cobra.Command, args []string) error { + serviceCommand.InputArgs = args + serviceCommand.GlobalFlags = MainGlobalOpts + return serviceCmd(&serviceCommand) + }, + } +) + +func init() { + serviceCommand.Command = _serviceCommand + serviceCommand.SetHelpTemplate(HelpTemplate()) + serviceCommand.SetUsageTemplate(UsageTemplate()) + flags := serviceCommand.Flags() + flags.Int64VarP(&serviceCommand.Timeout, "timeout", "t", 1000, "Time until the service session expires in milliseconds. Use 0 to disable the timeout") + flags.BoolVar(&serviceCommand.Varlink, "varlink", false, "Use legacy varlink service instead of REST") +} + +func serviceCmd(c *cliconfig.ServiceValues) error { + // For V2, default to the REST socket + apiURI := adapter.DefaultAPIAddress + if c.Varlink { + apiURI = adapter.DefaultVarlinkAddress + } + + if rootless.IsRootless() { + xdg, err := util.GetRuntimeDir() + if err != nil { + return err + } + socketName := "podman.sock" + if c.Varlink { + socketName = "io.podman" + } + socketDir := filepath.Join(xdg, "podman", socketName) + if _, err := os.Stat(filepath.Dir(socketDir)); err != nil { + if os.IsNotExist(err) { + if err := os.Mkdir(filepath.Dir(socketDir), 0755); err != nil { + return err + } + } else { + return err + } + } + apiURI = fmt.Sprintf("unix:%s", socketDir) + } + + if len(c.InputArgs) > 0 { + apiURI = c.InputArgs[0] + } + + logrus.Infof("using API endpoint: %s", apiURI) + + // Create a single runtime api consumption + runtime, err := libpodruntime.GetRuntimeDisableFDs(getContext(), &c.PodmanCommand) + if err != nil { + return errors.Wrapf(err, "error creating libpod runtime") + } + defer runtime.DeferredShutdown(false) + + timeout := time.Duration(c.Timeout) * time.Millisecond + if c.Varlink { + return runVarlink(runtime, apiURI, timeout, c) + } + return runREST(runtime, apiURI, timeout) +} + +func runREST(r *libpod.Runtime, uri string, timeout time.Duration) error { + logrus.Warn("This function is EXPERIMENTAL") + fmt.Println("This function is EXPERIMENTAL.") + fields := strings.Split(uri, ":") + if len(fields) == 1 { + return errors.Errorf("%s is an invalid socket destination", uri) + } + address := strings.Join(fields[1:], ":") + l, err := net.Listen(fields[0], address) + if err != nil { + return errors.Wrapf(err, "unable to create socket %s", uri) + } + server, err := api.NewServerWithSettings(r, timeout, &l) + if err != nil { + return err + } + return server.Serve() +} + +func runVarlink(r *libpod.Runtime, uri string, timeout time.Duration, c *cliconfig.ServiceValues) error { + var varlinkInterfaces = []*iopodman.VarlinkInterface{varlinkapi.New(&c.PodmanCommand, r)} + service, err := varlink.NewService( + "Atomic", + "podman", + version.Version, + "https://github.com/containers/libpod", + ) + if err != nil { + return errors.Wrapf(err, "unable to create new varlink service") + } + + for _, i := range varlinkInterfaces { + if err := service.RegisterInterface(i); err != nil { + return errors.Errorf("unable to register varlink interface %v", i) + } + } + + // Run the varlink server at the given address + if err = service.Listen(uri, timeout); err != nil { + switch err.(type) { + case varlink.ServiceTimeoutError: + logrus.Infof("varlink service expired (use --timeout to increase session time beyond %d ms, 0 means never timeout)", timeout.String()) + return nil + default: + return errors.Wrapf(err, "unable to start varlink service") + } + } + return nil +} diff --git a/cmd/podman/service_dummy.go b/cmd/podman/service_dummy.go new file mode 100644 index 000000000..a774c34de --- /dev/null +++ b/cmd/podman/service_dummy.go @@ -0,0 +1,11 @@ +// +build !varlink + +package main + +import "github.com/spf13/cobra" + +var ( + _serviceCommand = &cobra.Command{ + Use: "", + } +) diff --git a/cmd/podman/varlink.go b/cmd/podman/varlink.go index cd21e3574..047d94fc2 100644 --- a/cmd/podman/varlink.go +++ b/cmd/podman/varlink.go @@ -51,7 +51,7 @@ func init() { } func varlinkCmd(c *cliconfig.VarlinkValues) error { - varlinkURI := adapter.DefaultAddress + varlinkURI := adapter.DefaultVarlinkAddress if rootless.IsRootless() { xdg, err := util.GetRuntimeDir() if err != nil { diff --git a/cmd/service/main.go b/cmd/service/main.go deleted file mode 100644 index 0290de892..000000000 --- a/cmd/service/main.go +++ /dev/null @@ -1,55 +0,0 @@ -package main - -import ( - "context" - "fmt" - "os" - - "github.com/containers/libpod/cmd/podman/cliconfig" - "github.com/containers/libpod/cmd/podman/libpodruntime" - api "github.com/containers/libpod/pkg/api/server" - "github.com/containers/storage/pkg/reexec" - log "github.com/sirupsen/logrus" - "github.com/spf13/cobra" -) - -func initConfig() { - // we can do more stuff in here. -} - -func main() { - if reexec.Init() { - // We were invoked with a different argv[0] indicating that we - // had a specific job to do as a subprocess, and it's done. - return - } - - cobra.OnInitialize(initConfig) - log.SetLevel(log.DebugLevel) - - config := cliconfig.PodmanCommand{ - Command: &cobra.Command{}, - InputArgs: []string{}, - GlobalFlags: cliconfig.MainFlags{}, - Remote: false, - } - // Create a single runtime for http - runtime, err := libpodruntime.GetRuntimeDisableFDs(context.Background(), &config) - if err != nil { - fmt.Printf("error creating libpod runtime: %s", err.Error()) - os.Exit(1) - } - defer runtime.DeferredShutdown(false) - - server, err := api.NewServer(runtime) - if err != nil { - fmt.Println(err.Error()) - os.Exit(1) - } - - err = server.Serve() - if err != nil { - fmt.Println(err.Error()) - os.Exit(1) - } -} diff --git a/commands.md b/commands.md index b744b702e..17e069cb1 100644 --- a/commands.md +++ b/commands.md @@ -73,6 +73,7 @@ | [podman-rmi(1)](/docs/source/markdown/podman-rmi.1.md) | Removes one or more images | | [podman-run(1)](/docs/source/markdown/podman-run.1.md) | Run a command in a container | | [podman-save(1)](/docs/source/markdown/podman-save.1.md) | Saves an image to an archive | +| [podman-service(1)](/docs/source/markdown/podman-service.1.md) | Run an API listening service | | [podman-search(1)](/docs/source/markdown/podman-search.1.md) | Search a registry for an image | | [podman-start(1)](/docs/source/markdown/podman-start.1.md) | Starts one or more containers | | [podman-stats(1)](/docs/source/markdown/podman-stats.1.md) | Display a live stream of one or more containers' resource usage statistics | diff --git a/completions/bash/podman b/completions/bash/podman index ca3618b0b..57b9547a7 100644 --- a/completions/bash/podman +++ b/completions/bash/podman @@ -1755,6 +1755,19 @@ _podman_search() { _complete_ "$options_with_args" "$boolean_options" } +_podman_service() { + local options_with_args=" + -t + --timeout + " + local boolean_options=" + --help + -h + --varlink + " + _complete_ "$options_with_args" "$boolean_options" +} + _podman_unmount() { _podman_umount $@ } diff --git a/docs/source/markdown/podman-service.1.md b/docs/source/markdown/podman-service.1.md new file mode 100644 index 000000000..5c55e20d3 --- /dev/null +++ b/docs/source/markdown/podman-service.1.md @@ -0,0 +1,47 @@ +% podman-service(1) + +## NAME +podman\-service - Run an API service + +## SYNOPSIS +**podman service** [*options*] + +## DESCRIPTION +The **podman service** command creates a listening service that will answer API calls for Podman. You may +optionally provide an endpoint for the API in URI form. For example, *unix://tmp/foobar.sock* or *tcp:localhost:8080*. +If no endpoint is provided, defaults will be used. The default endpoint for a rootfull +service is *unix:/run/podman/podman.sock* and rootless is *unix:/$XDG_RUNTIME_DIR/podman/podman.sock* (for +example *unix:/run/user/1000/podman/podman.sock*) + +## OPTIONS + +**--timeout**, **-t** + +The time until the session expires in _milliseconds_. The default is 1 +second. A value of `0` means no timeout and the session will not expire. + +**--varlink** + +Use the varlink protocol instead of the REST-based protocol. This option will be deprecated in the future. + +**--help**, **-h** + +Print usage statement. + +## EXAMPLES + +Run an API listening for 5 seconds using the default socket. +``` +podman service --timeout 5000 +``` + +Run the podman varlink service with an alternate URI and accept the default timeout. +``` +$ podman service --varlink unix:/tmp/io.podman +``` + +## SEE ALSO +podman(1), podman-varlink(1) + +## HISTORY +January 2020, Originally compiled by Brent Baude<bbaude@redhat.com> diff --git a/docs/source/markdown/podman.1.md b/docs/source/markdown/podman.1.md index 0c9ec3d1c..6e0eff045 100644 --- a/docs/source/markdown/podman.1.md +++ b/docs/source/markdown/podman.1.md @@ -191,6 +191,7 @@ the exit codes follow the `chroot` standard, see below: | [podman-rmi(1)](podman-rmi.1.md) | Removes one or more locally stored images. | | [podman-run(1)](podman-run.1.md) | Run a command in a new container. | | [podman-save(1)](podman-save.1.md) | Save an image to a container archive. | +| [podman-service(1)](podman-service.1.md) | Run an API service | | [podman-search(1)](podman-search.1.md) | Search a registry for an image. | | [podman-start(1)](podman-start.1.md) | Start one or more containers. | | [podman-stats(1)](podman-stats.1.md) | Display a live stream of one or more container's resource usage statistics. | diff --git a/pkg/adapter/client.go b/pkg/adapter/client.go index da4670892..5774ebe72 100644 --- a/pkg/adapter/client.go +++ b/pkg/adapter/client.go @@ -57,7 +57,7 @@ func (r RemoteRuntime) RemoteEndpoint() (remoteEndpoint *Endpoint, err error) { // last resort is to make a socket connection with the default varlink address for root user } else { logrus.Debug("creating a varlink address based default root address") - remoteEndpoint, err = newSocketConnection(DefaultAddress) + remoteEndpoint, err = newSocketConnection(DefaultVarlinkAddress) } return } diff --git a/pkg/adapter/client_config.go b/pkg/adapter/client_config.go index 3559b16e3..8187b03b1 100644 --- a/pkg/adapter/client_config.go +++ b/pkg/adapter/client_config.go @@ -1,7 +1,10 @@ package adapter -// DefaultAddress is the default address of the varlink socket -const DefaultAddress = "unix:/run/podman/io.podman" +// DefaultAPIAddress is the default address of the REST socket +const DefaultAPIAddress = "unix:/run/podman/podman.sock" + +// DefaultVarlinkAddress is the default address of the varlink socket +const DefaultVarlinkAddress = "unix:/run/podman/io.podman" // EndpointType declares the type of server connection type EndpointType int diff --git a/pkg/api/server/listener_api.go b/pkg/api/server/listener_api.go new file mode 100644 index 000000000..4984216b8 --- /dev/null +++ b/pkg/api/server/listener_api.go @@ -0,0 +1,31 @@ +package server + +import ( + "net" + "os" + "path/filepath" + + "github.com/pkg/errors" +) + +// ListenUnix follows stdlib net.Listen() API, providing a unix listener for given path +// ListenUnix will delete and create files/directories as needed +func ListenUnix(network string, path string) (net.Listener, error) { + // setup custom listener for API server + err := os.MkdirAll(filepath.Dir(path), 0770) + if err != nil { + return nil, errors.Wrapf(err, "api.ListenUnix() failed to create %s", filepath.Dir(path)) + } + os.Remove(path) + + listener, err := net.Listen(network, path) + if err != nil { + return nil, errors.Wrapf(err, "api.ListenUnix() failed to create net.Listen(%s, %s)", network, path) + } + + _, err = os.Stat(path) + if err != nil { + return nil, errors.Wrapf(err, "net.Listen(%s, %s) failed to report the failure to create socket", network, path) + } + return listener, nil +} diff --git a/pkg/api/server/server.go b/pkg/api/server/server.go index 9abedb359..f3bae0345 100644 --- a/pkg/api/server/server.go +++ b/pkg/api/server/server.go @@ -54,9 +54,9 @@ import ( ) type APIServer struct { - http.Server // Where the HTTP work happens + http.Server // The HTTP work happens here *schema.Decoder // Decoder for Query parameters to structs - context.Context // Context for graceful server shutdown + context.Context // Context to carry objects to handlers *libpod.Runtime // Where the real work happens net.Listener // mux for routing HTTP API calls to libpod routines context.CancelFunc // Stop APIServer @@ -64,14 +64,37 @@ type APIServer struct { time.Duration // Duration of client access sliding window } -// NewServer will create and configure a new API HTTP server +// Number of seconds to wait for next request, if exceeded shutdown server +const ( + DefaultServiceDuration = 300 * time.Second + UnlimitedServiceDuration = 0 * time.Second +) + +// NewServer will create and configure a new API server with all defaults func NewServer(runtime *libpod.Runtime) (*APIServer, error) { - listeners, err := activation.Listeners() - if err != nil { - return nil, errors.Wrap(err, "Cannot retrieve file descriptors from systemd") - } - if len(listeners) != 1 { - return nil, errors.Errorf("Wrong number of file descriptors from systemd for socket activation (%d != 1)", len(listeners)) + return newServer(runtime, DefaultServiceDuration, nil) +} + +// NewServerWithSettings will create and configure a new API server using provided settings +func NewServerWithSettings(runtime *libpod.Runtime, duration time.Duration, listener *net.Listener) (*APIServer, error) { + return newServer(runtime, duration, listener) +} + +func newServer(runtime *libpod.Runtime, duration time.Duration, listener *net.Listener) (*APIServer, error) { + // If listener not provided try socket activation protocol + if listener == nil { + if _, found := os.LookupEnv("LISTEN_FDS"); !found { + return nil, errors.Errorf("Cannot create Server, no listener provided and socket activation protocol is not active.") + } + + listeners, err := activation.Listeners() + if err != nil { + return nil, errors.Wrap(err, "Cannot retrieve file descriptors from systemd") + } + if len(listeners) != 1 { + return nil, errors.Errorf("Wrong number of file descriptors for socket activation protocol (%d != 1)", len(listeners)) + } + listener = &listeners[0] } router := mux.NewRouter() @@ -86,9 +109,9 @@ func NewServer(runtime *libpod.Runtime) (*APIServer, error) { Decoder: schema.NewDecoder(), Context: nil, Runtime: runtime, - Listener: listeners[0], + Listener: *listener, CancelFunc: nil, - Duration: 300 * time.Second, + Duration: duration, } server.Timer = time.AfterFunc(server.Duration, func() { if err := server.Shutdown(); err != nil { @@ -182,6 +205,11 @@ func (s *APIServer) Serve() error { // Shutdown is a clean shutdown waiting on existing clients func (s *APIServer) Shutdown() error { + // Duration == 0 flags no auto-shutdown of server + if s.Duration == 0 { + return nil + } + // We're still in the sliding service window if s.Timer.Stop() { s.Timer.Reset(s.Duration) diff --git a/pkg/bindings/containers.go b/pkg/bindings/containers.go index 01f68f970..057580088 100644 --- a/pkg/bindings/containers.go +++ b/pkg/bindings/containers.go @@ -126,11 +126,11 @@ func (c Connection) ContainerExists(nameOrID string) (bool, error) { return false, nil } -func (c Connection) StopContainer(nameOrID string, timeout int) error { - // TODO we might need to distinguish whether a timeout is desired; a zero, the int - // zero value is valid; what do folks want to do? +func (c Connection) StopContainer(nameOrID string, timeout *int) error { params := make(map[string]string) - params["t"] = strconv.Itoa(timeout) + if timeout != nil { + params["t"] = strconv.Itoa(*timeout) + } response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/containers/%s/stop", nameOrID), nil, params) if err != nil { return err |