This fix tries to address the issue raised in 29972 where
it was not possible to specify `--read-only` for `docker service create`
and `docker service update`, in order to have the container's root file
system to be read only.
This fix adds `--read-only` and update the `ReadonlyRootfs` in `HostConfig`
through `service create` and `service update`.
Related docs has been updated.
Integration test has been added.
This fix fixes 29972.
Signed-off-by: Yong Tang <yong.tang.github@outlook.com>
| ... | ... |
@@ -1884,6 +1884,9 @@ definitions: |
| 1884 | 1884 |
TTY: |
| 1885 | 1885 |
description: "Whether a pseudo-TTY should be allocated." |
| 1886 | 1886 |
type: "boolean" |
| 1887 |
+ ReadOnly: |
|
| 1888 |
+ description: "Mount the container's root filesystem as read only." |
|
| 1889 |
+ type: "boolean" |
|
| 1887 | 1890 |
Mounts: |
| 1888 | 1891 |
description: "Specification for mounts to be added to containers created as part of the service." |
| 1889 | 1892 |
type: "array" |
| ... | ... |
@@ -34,6 +34,7 @@ type ContainerSpec struct {
|
| 34 | 34 |
Groups []string `json:",omitempty"` |
| 35 | 35 |
TTY bool `json:",omitempty"` |
| 36 | 36 |
OpenStdin bool `json:",omitempty"` |
| 37 |
+ ReadOnly bool `json:",omitempty"` |
|
| 37 | 38 |
Mounts []mount.Mount `json:",omitempty"` |
| 38 | 39 |
StopGracePeriod *time.Duration `json:",omitempty"` |
| 39 | 40 |
Healthcheck *container.HealthConfig `json:",omitempty"` |
| ... | ... |
@@ -303,6 +303,7 @@ type serviceOptions struct {
|
| 303 | 303 |
user string |
| 304 | 304 |
groups opts.ListOpts |
| 305 | 305 |
tty bool |
| 306 |
+ readOnly bool |
|
| 306 | 307 |
mounts opts.MountOpt |
| 307 | 308 |
dns opts.ListOpts |
| 308 | 309 |
dnsSearch opts.ListOpts |
| ... | ... |
@@ -384,6 +385,7 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) {
|
| 384 | 384 |
User: opts.user, |
| 385 | 385 |
Groups: opts.groups.GetAll(), |
| 386 | 386 |
TTY: opts.tty, |
| 387 |
+ ReadOnly: opts.readOnly, |
|
| 387 | 388 |
Mounts: opts.mounts.Value(), |
| 388 | 389 |
DNSConfig: &swarm.DNSConfig{
|
| 389 | 390 |
Nameservers: opts.dns.GetAll(), |
| ... | ... |
@@ -488,6 +490,9 @@ func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) {
|
| 488 | 488 |
|
| 489 | 489 |
flags.BoolVarP(&opts.tty, flagTTY, "t", false, "Allocate a pseudo-TTY") |
| 490 | 490 |
flags.SetAnnotation(flagTTY, "version", []string{"1.25"})
|
| 491 |
+ |
|
| 492 |
+ flags.BoolVar(&opts.readOnly, flagReadOnly, false, "Mount the container's root filesystem as read only") |
|
| 493 |
+ flags.SetAnnotation(flagReadOnly, "version", []string{"1.26"})
|
|
| 491 | 494 |
} |
| 492 | 495 |
|
| 493 | 496 |
const ( |
| ... | ... |
@@ -532,6 +537,7 @@ const ( |
| 532 | 532 |
flagPublish = "publish" |
| 533 | 533 |
flagPublishRemove = "publish-rm" |
| 534 | 534 |
flagPublishAdd = "publish-add" |
| 535 |
+ flagReadOnly = "read-only" |
|
| 535 | 536 |
flagReplicas = "replicas" |
| 536 | 537 |
flagReserveCPU = "reserve-cpu" |
| 537 | 538 |
flagReserveMemory = "reserve-memory" |
| ... | ... |
@@ -341,6 +341,14 @@ func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error {
|
| 341 | 341 |
cspec.TTY = tty |
| 342 | 342 |
} |
| 343 | 343 |
|
| 344 |
+ if flags.Changed(flagReadOnly) {
|
|
| 345 |
+ readOnly, err := flags.GetBool(flagReadOnly) |
|
| 346 |
+ if err != nil {
|
|
| 347 |
+ return err |
|
| 348 |
+ } |
|
| 349 |
+ cspec.ReadOnly = readOnly |
|
| 350 |
+ } |
|
| 351 |
+ |
|
| 344 | 352 |
return nil |
| 345 | 353 |
} |
| 346 | 354 |
|
| ... | ... |
@@ -442,3 +442,25 @@ func TestUpdateSecretUpdateInPlace(t *testing.T) {
|
| 442 | 442 |
assert.Equal(t, updatedSecrets[0].SecretName, "foo") |
| 443 | 443 |
assert.Equal(t, updatedSecrets[0].File.Name, "foo2") |
| 444 | 444 |
} |
| 445 |
+ |
|
| 446 |
+func TestUpdateReadOnly(t *testing.T) {
|
|
| 447 |
+ spec := &swarm.ServiceSpec{}
|
|
| 448 |
+ cspec := &spec.TaskTemplate.ContainerSpec |
|
| 449 |
+ |
|
| 450 |
+ // Update with --read-only=true, changed to true |
|
| 451 |
+ flags := newUpdateCommand(nil).Flags() |
|
| 452 |
+ flags.Set("read-only", "true")
|
|
| 453 |
+ updateService(flags, spec) |
|
| 454 |
+ assert.Equal(t, cspec.ReadOnly, true) |
|
| 455 |
+ |
|
| 456 |
+ // Update without --read-only, no change |
|
| 457 |
+ flags = newUpdateCommand(nil).Flags() |
|
| 458 |
+ updateService(flags, spec) |
|
| 459 |
+ assert.Equal(t, cspec.ReadOnly, true) |
|
| 460 |
+ |
|
| 461 |
+ // Update with --read-only=false, changed to false |
|
| 462 |
+ flags = newUpdateCommand(nil).Flags() |
|
| 463 |
+ flags.Set("read-only", "false")
|
|
| 464 |
+ updateService(flags, spec) |
|
| 465 |
+ assert.Equal(t, cspec.ReadOnly, false) |
|
| 466 |
+} |
| ... | ... |
@@ -25,6 +25,7 @@ func containerSpecFromGRPC(c *swarmapi.ContainerSpec) types.ContainerSpec {
|
| 25 | 25 |
Groups: c.Groups, |
| 26 | 26 |
TTY: c.TTY, |
| 27 | 27 |
OpenStdin: c.OpenStdin, |
| 28 |
+ ReadOnly: c.ReadOnly, |
|
| 28 | 29 |
Hosts: c.Hosts, |
| 29 | 30 |
Secrets: secretReferencesFromGRPC(c.Secrets), |
| 30 | 31 |
} |
| ... | ... |
@@ -146,6 +147,7 @@ func containerToGRPC(c types.ContainerSpec) (*swarmapi.ContainerSpec, error) {
|
| 146 | 146 |
Groups: c.Groups, |
| 147 | 147 |
TTY: c.TTY, |
| 148 | 148 |
OpenStdin: c.OpenStdin, |
| 149 |
+ ReadOnly: c.ReadOnly, |
|
| 149 | 150 |
Hosts: c.Hosts, |
| 150 | 151 |
Secrets: secretReferencesToGRPC(c.Secrets), |
| 151 | 152 |
} |
| ... | ... |
@@ -335,10 +335,11 @@ func (c *containerConfig) healthcheck() *enginecontainer.HealthConfig {
|
| 335 | 335 |
|
| 336 | 336 |
func (c *containerConfig) hostConfig() *enginecontainer.HostConfig {
|
| 337 | 337 |
hc := &enginecontainer.HostConfig{
|
| 338 |
- Resources: c.resources(), |
|
| 339 |
- GroupAdd: c.spec().Groups, |
|
| 340 |
- PortBindings: c.portBindings(), |
|
| 341 |
- Mounts: c.mounts(), |
|
| 338 |
+ Resources: c.resources(), |
|
| 339 |
+ GroupAdd: c.spec().Groups, |
|
| 340 |
+ PortBindings: c.portBindings(), |
|
| 341 |
+ Mounts: c.mounts(), |
|
| 342 |
+ ReadonlyRootfs: c.spec().ReadOnly, |
|
| 342 | 343 |
} |
| 343 | 344 |
|
| 344 | 345 |
if c.spec().DNSConfig != nil {
|
| ... | ... |
@@ -48,6 +48,7 @@ Options: |
| 48 | 48 |
--network list Network attachments (default []) |
| 49 | 49 |
--no-healthcheck Disable any container-specified HEALTHCHECK |
| 50 | 50 |
-p, --publish port Publish a port as a node port |
| 51 |
+ --read-only Mount the container's root filesystem as read only |
|
| 51 | 52 |
--replicas uint Number of tasks |
| 52 | 53 |
--reserve-cpu decimal Reserve CPUs (default 0.000) |
| 53 | 54 |
--reserve-memory bytes Reserve Memory (default 0 B) |
| ... | ... |
@@ -58,6 +58,7 @@ Options: |
| 58 | 58 |
--no-healthcheck Disable any container-specified HEALTHCHECK |
| 59 | 59 |
--publish-add port Add or update a published port |
| 60 | 60 |
--publish-rm port Remove a published port by its target port |
| 61 |
+ --read-only Mount the container's root filesystem as read only |
|
| 61 | 62 |
--replicas uint Number of tasks |
| 62 | 63 |
--reserve-cpu decimal Reserve CPUs (default 0.000) |
| 63 | 64 |
--reserve-memory bytes Reserve Memory (default 0 B) |
| ... | ... |
@@ -1644,3 +1644,24 @@ func (s *DockerSwarmSuite) TestSwarmInitWithDrain(c *check.C) {
|
| 1644 | 1644 |
c.Assert(err, checker.IsNil) |
| 1645 | 1645 |
c.Assert(out, checker.Contains, "Drain") |
| 1646 | 1646 |
} |
| 1647 |
+ |
|
| 1648 |
+func (s *DockerSwarmSuite) TestSwarmReadonlyRootfs(c *check.C) {
|
|
| 1649 |
+ testRequires(c, DaemonIsLinux, UserNamespaceROMount) |
|
| 1650 |
+ |
|
| 1651 |
+ d := s.AddDaemon(c, true, true) |
|
| 1652 |
+ |
|
| 1653 |
+ out, err := d.Cmd("service", "create", "--name", "top", "--read-only", "busybox", "top")
|
|
| 1654 |
+ c.Assert(err, checker.IsNil, check.Commentf(out)) |
|
| 1655 |
+ |
|
| 1656 |
+ // make sure task has been deployed. |
|
| 1657 |
+ waitAndAssert(c, defaultReconciliationTimeout, d.CheckActiveContainerCount, checker.Equals, 1) |
|
| 1658 |
+ |
|
| 1659 |
+ out, err = d.Cmd("service", "inspect", "--format", "{{ .Spec.TaskTemplate.ContainerSpec.ReadOnly }}", "top")
|
|
| 1660 |
+ c.Assert(err, checker.IsNil, check.Commentf(out)) |
|
| 1661 |
+ c.Assert(strings.TrimSpace(out), checker.Equals, "true") |
|
| 1662 |
+ |
|
| 1663 |
+ containers := d.ActiveContainers() |
|
| 1664 |
+ out, err = d.Cmd("inspect", "--type", "container", "--format", "{{.HostConfig.ReadonlyRootfs}}", containers[0])
|
|
| 1665 |
+ c.Assert(err, checker.IsNil, check.Commentf(out)) |
|
| 1666 |
+ c.Assert(strings.TrimSpace(out), checker.Equals, "true") |
|
| 1667 |
+} |