Browse code

Add `--read-only` for `service create` and `service update`

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>

Yong Tang authored on 2017/01/14 17:12:19
Showing 10 changed files
... ...
@@ -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
+}