diff --git a/cli/command/service/opts.go b/cli/command/service/opts.go index 2199e9f363..989fd18b8f 100644 --- a/cli/command/service/opts.go +++ b/cli/command/service/opts.go @@ -294,6 +294,7 @@ type serviceOptions struct { workdir string user string groups []string + tty bool mounts opts.MountOpt resources resourceOptions @@ -365,6 +366,7 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) { Dir: opts.workdir, User: opts.user, Groups: opts.groups, + TTY: opts.tty, Mounts: opts.mounts.Value(), StopGracePeriod: opts.stopGrace.Value(), }, @@ -450,6 +452,8 @@ func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) { flags.Var(&opts.healthcheck.timeout, flagHealthTimeout, "Maximum time to allow one check to run") flags.IntVar(&opts.healthcheck.retries, flagHealthRetries, 0, "Consecutive failures needed to report unhealthy") flags.BoolVar(&opts.healthcheck.noHealthcheck, flagNoHealthcheck, false, "Disable any container-specified HEALTHCHECK") + + flags.BoolVarP(&opts.tty, flagTTY, "t", false, "Allocate a pseudo-TTY") } const ( @@ -490,6 +494,7 @@ const ( flagRestartMaxAttempts = "restart-max-attempts" flagRestartWindow = "restart-window" flagStopGracePeriod = "stop-grace-period" + flagTTY = "tty" flagUpdateDelay = "update-delay" flagUpdateFailureAction = "update-failure-action" flagUpdateMaxFailureRatio = "update-max-failure-ratio" diff --git a/cli/command/service/update.go b/cli/command/service/update.go index 34cc9bc3d8..c278ac1ba4 100644 --- a/cli/command/service/update.go +++ b/cli/command/service/update.go @@ -274,6 +274,14 @@ func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error { return err } + if flags.Changed(flagTTY) { + tty, err := flags.GetBool(flagTTY) + if err != nil { + return err + } + cspec.TTY = tty + } + return nil } diff --git a/daemon/cluster/convert/container.go b/daemon/cluster/convert/container.go index 5e0cd3c49d..0ce4fa371f 100644 --- a/daemon/cluster/convert/container.go +++ b/daemon/cluster/convert/container.go @@ -22,6 +22,7 @@ func containerSpecFromGRPC(c *swarmapi.ContainerSpec) types.ContainerSpec { Dir: c.Dir, User: c.User, Groups: c.Groups, + TTY: c.TTY, } // Mounts @@ -77,6 +78,7 @@ func containerToGRPC(c types.ContainerSpec) (*swarmapi.ContainerSpec, error) { Dir: c.Dir, User: c.User, Groups: c.Groups, + TTY: c.TTY, } if c.StopGracePeriod != nil { diff --git a/daemon/cluster/executor/container/container.go b/daemon/cluster/executor/container/container.go index fb0ab489d2..68dfce7dcd 100644 --- a/daemon/cluster/executor/container/container.go +++ b/daemon/cluster/executor/container/container.go @@ -127,6 +127,7 @@ func (c *containerConfig) image() string { func (c *containerConfig) config() *enginecontainer.Config { config := &enginecontainer.Config{ Labels: c.labels(), + Tty: c.spec().TTY, User: c.spec().User, Hostname: c.spec().Hostname, Env: c.spec().Env, diff --git a/docs/reference/api/docker_remote_api.md b/docs/reference/api/docker_remote_api.md index 1e1dd1ee25..99aac60955 100644 --- a/docs/reference/api/docker_remote_api.md +++ b/docs/reference/api/docker_remote_api.md @@ -167,6 +167,7 @@ This section lists each version from latest to oldest. Each listing includes a * The `HostConfig` field now includes `NanoCPUs` that represents CPU quota in units of 10-9 CPUs. * `GET /info` now returns more structured information about security options. * The `HostConfig` field now includes `CpuCount` that represents the number of CPUs available for execution by the container. Windows daemon only. +* `POST /services/create` and `POST /services/(id or name)/update` now accept the `TTY` parameter, which allocate a pseudo-TTY in container. ### v1.24 API changes diff --git a/docs/reference/api/docker_remote_api_v1.25.md b/docs/reference/api/docker_remote_api_v1.25.md index a71c5875d5..544390c971 100644 --- a/docs/reference/api/docker_remote_api_v1.25.md +++ b/docs/reference/api/docker_remote_api_v1.25.md @@ -5112,7 +5112,8 @@ image](#create-an-image) section for more details. } } ], - "User": "33" + "User": "33", + "TTY": false }, "LogDriver": { "Name": "json-file", @@ -5189,6 +5190,7 @@ image](#create-an-image) section for more details. - **User** – A string value specifying the user inside the container. - **Labels** – A map of labels to associate with the service (e.g., `{"key":"value", "key2":"value2"}`). + - **TTY** – A boolean indicating whether a pseudo-TTY should be allocated. - **Mounts** – Specification for mounts to be added to containers created as part of the service. - **Target** – Container path. @@ -5390,7 +5392,8 @@ image](#create-an-image) section for more details. "Image": "busybox", "Args": [ "top" - ] + ], + "TTY": true }, "Resources": { "Limits": {}, @@ -5438,6 +5441,7 @@ image](#create-an-image) section for more details. - **User** – A string value specifying the user inside the container. - **Labels** – A map of labels to associate with the service (e.g., `{"key":"value", "key2":"value2"}`). + - **TTY** – A boolean indicating whether a pseudo-TTY should be allocated. - **Mounts** – Specification for mounts to be added to containers created as part of the new service. - **Target** – Container path. diff --git a/docs/reference/commandline/service_create.md b/docs/reference/commandline/service_create.md index f5f203d5cc..17bb27d71b 100644 --- a/docs/reference/commandline/service_create.md +++ b/docs/reference/commandline/service_create.md @@ -52,6 +52,7 @@ Options: --restart-max-attempts value Maximum number of restarts before giving up (default none) --restart-window value Window used to evaluate the restart policy (default none) --stop-grace-period value Time to wait before force killing a container (default none) + -t, --tty Allocate a pseudo-TTY --update-delay duration Delay between updates --update-failure-action string Action on update failure (pause|continue) (default "pause") --update-max-failure-ratio value Failure rate to tolerate during an update diff --git a/docs/reference/commandline/service_update.md b/docs/reference/commandline/service_update.md index f938a9efc3..143e435157 100644 --- a/docs/reference/commandline/service_update.md +++ b/docs/reference/commandline/service_update.md @@ -58,6 +58,7 @@ Options: --restart-window value Window used to evaluate the restart policy (default none) --rollback Rollback to previous specification --stop-grace-period value Time to wait before force killing a container (default none) + -t, --tty Allocate a pseudo-TTY --update-delay duration Delay between updates --update-failure-action string Action on update failure (pause|continue) (default "pause") --update-max-failure-ratio value Failure rate to tolerate during an update diff --git a/integration-cli/docker_cli_swarm_test.go b/integration-cli/docker_cli_swarm_test.go index ab0f609701..6bb4e634b4 100644 --- a/integration-cli/docker_cli_swarm_test.go +++ b/integration-cli/docker_cli_swarm_test.go @@ -718,3 +718,74 @@ func (s *DockerSwarmSuite) TestSwarmServiceEnvFile(c *check.C) { c.Assert(err, checker.IsNil) c.Assert(out, checker.Contains, "[VAR1=C VAR2]") } + +func (s *DockerSwarmSuite) TestSwarmServiceTTY(c *check.C) { + d := s.AddDaemon(c, true, true) + + name := "top" + + ttyCheck := "if [ -t 0 ]; then echo TTY > /status && top; else echo none > /status && top; fi" + + // Without --tty + expectedOutput := "none" + out, err := d.Cmd("service", "create", "--name", name, "busybox", "sh", "-c", ttyCheck) + c.Assert(err, checker.IsNil) + + // Make sure task has been deployed. + waitAndAssert(c, defaultReconciliationTimeout, d.checkActiveContainerCount, checker.Equals, 1) + + // We need to get the container id. + out, err = d.Cmd("ps", "-a", "-q", "--no-trunc") + c.Assert(err, checker.IsNil) + id := strings.TrimSpace(out) + + out, err = d.Cmd("exec", id, "cat", "/status") + c.Assert(err, checker.IsNil) + c.Assert(out, checker.Contains, expectedOutput, check.Commentf("Expected '%s', but got %q", expectedOutput, out)) + + // Remove service + out, err = d.Cmd("service", "rm", name) + c.Assert(err, checker.IsNil) + // Make sure container has been destroyed. + waitAndAssert(c, defaultReconciliationTimeout, d.checkActiveContainerCount, checker.Equals, 0) + + // With --tty + expectedOutput = "TTY" + out, err = d.Cmd("service", "create", "--name", name, "--tty", "busybox", "sh", "-c", ttyCheck) + c.Assert(err, checker.IsNil) + + // Make sure task has been deployed. + waitAndAssert(c, defaultReconciliationTimeout, d.checkActiveContainerCount, checker.Equals, 1) + + // We need to get the container id. + out, err = d.Cmd("ps", "-a", "-q", "--no-trunc") + c.Assert(err, checker.IsNil) + id = strings.TrimSpace(out) + + out, err = d.Cmd("exec", id, "cat", "/status") + c.Assert(err, checker.IsNil) + c.Assert(out, checker.Contains, expectedOutput, check.Commentf("Expected '%s', but got %q", expectedOutput, out)) +} + +func (s *DockerSwarmSuite) TestSwarmServiceTTYUpdate(c *check.C) { + d := s.AddDaemon(c, true, true) + + // Create a service + name := "top" + _, err := d.Cmd("service", "create", "--name", name, "busybox", "top") + c.Assert(err, checker.IsNil) + + // Make sure task has been deployed. + waitAndAssert(c, defaultReconciliationTimeout, d.checkActiveContainerCount, checker.Equals, 1) + + out, err := d.Cmd("service", "inspect", "--format", "{{ .Spec.TaskTemplate.ContainerSpec.TTY }}", name) + c.Assert(err, checker.IsNil) + c.Assert(strings.TrimSpace(out), checker.Equals, "false") + + _, err = d.Cmd("service", "update", "--tty", name) + c.Assert(err, checker.IsNil) + + out, err = d.Cmd("service", "inspect", "--format", "{{ .Spec.TaskTemplate.ContainerSpec.TTY }}", name) + c.Assert(err, checker.IsNil) + c.Assert(strings.TrimSpace(out), checker.Equals, "true") +}