From e401b88e59e098745744917c555d549f08353e6d Mon Sep 17 00:00:00 2001
From: Vincent Demeester <vincent@sbr.pm>
Date: Fri, 1 Jun 2018 12:47:38 +0200
Subject: [PATCH] Add support for `init` on services

It's already supported by `swarmkit`, and act the same as
`HostConfig.Init` on container creation.

Signed-off-by: Vincent Demeester <vincent@sbr.pm>
---
 api/swagger.yaml                              |  4 ++
 api/types/swarm/container.go                  |  1 +
 daemon/cluster/convert/container.go           | 17 ++++++
 .../cluster/executor/container/container.go   |  9 ++++
 integration/internal/swarm/service.go         |  8 +++
 integration/service/create_test.go            | 54 +++++++++++++++++++
 internal/test/daemon/daemon.go                |  6 ++-
 internal/test/daemon/ops.go                   |  6 +++
 8 files changed, 104 insertions(+), 1 deletion(-)

diff --git a/api/swagger.yaml b/api/swagger.yaml
index 3cdfe2988c..86374415fd 100644
--- a/api/swagger.yaml
+++ b/api/swagger.yaml
@@ -2721,6 +2721,10 @@ definitions:
               - "default"
               - "process"
               - "hyperv"
+          Init:
+            description: "Run an init inside the container that forwards signals and reaps processes. This field is omitted if empty, and the default (as configured on the daemon) is used."
+            type: "boolean"
+            x-nullable: true
       NetworkAttachmentSpec:
         description: |
           Read-only spec type for non-swarm containers attached to swarm overlay
diff --git a/api/types/swarm/container.go b/api/types/swarm/container.go
index 0041653c9d..151211ff5a 100644
--- a/api/types/swarm/container.go
+++ b/api/types/swarm/container.go
@@ -55,6 +55,7 @@ type ContainerSpec struct {
 	User            string                  `json:",omitempty"`
 	Groups          []string                `json:",omitempty"`
 	Privileges      *Privileges             `json:",omitempty"`
+	Init            *bool                   `json:",omitempty"`
 	StopSignal      string                  `json:",omitempty"`
 	TTY             bool                    `json:",omitempty"`
 	OpenStdin       bool                    `json:",omitempty"`
diff --git a/daemon/cluster/convert/container.go b/daemon/cluster/convert/container.go
index 0a34fc73e4..d889b4004c 100644
--- a/daemon/cluster/convert/container.go
+++ b/daemon/cluster/convert/container.go
@@ -35,6 +35,7 @@ func containerSpecFromGRPC(c *swarmapi.ContainerSpec) *types.ContainerSpec {
 		Secrets:    secretReferencesFromGRPC(c.Secrets),
 		Configs:    configReferencesFromGRPC(c.Configs),
 		Isolation:  IsolationFromGRPC(c.Isolation),
+		Init:       initFromGRPC(c.Init),
 	}
 
 	if c.DNSConfig != nil {
@@ -119,6 +120,21 @@ func containerSpecFromGRPC(c *swarmapi.ContainerSpec) *types.ContainerSpec {
 	return containerSpec
 }
 
+func initFromGRPC(v *gogotypes.BoolValue) *bool {
+	if v == nil {
+		return nil
+	}
+	value := v.GetValue()
+	return &value
+}
+
+func initToGRPC(v *bool) *gogotypes.BoolValue {
+	if v == nil {
+		return nil
+	}
+	return &gogotypes.BoolValue{Value: *v}
+}
+
 func secretReferencesToGRPC(sr []*types.SecretReference) []*swarmapi.SecretReference {
 	refs := make([]*swarmapi.SecretReference, 0, len(sr))
 	for _, s := range sr {
@@ -234,6 +250,7 @@ func containerToGRPC(c *types.ContainerSpec) (*swarmapi.ContainerSpec, error) {
 		Secrets:    secretReferencesToGRPC(c.Secrets),
 		Configs:    configReferencesToGRPC(c.Configs),
 		Isolation:  isolationToGRPC(c.Isolation),
+		Init:       initToGRPC(c.Init),
 	}
 
 	if c.DNSConfig != nil {
diff --git a/daemon/cluster/executor/container/container.go b/daemon/cluster/executor/container/container.go
index 69d673bd30..77d21d2c1f 100644
--- a/daemon/cluster/executor/container/container.go
+++ b/daemon/cluster/executor/container/container.go
@@ -172,6 +172,14 @@ func (c *containerConfig) isolation() enginecontainer.Isolation {
 	return convert.IsolationFromGRPC(c.spec().Isolation)
 }
 
+func (c *containerConfig) init() *bool {
+	if c.spec().Init == nil {
+		return nil
+	}
+	init := c.spec().Init.GetValue()
+	return &init
+}
+
 func (c *containerConfig) exposedPorts() map[nat.Port]struct{} {
 	exposedPorts := make(map[nat.Port]struct{})
 	if c.task.Endpoint == nil {
@@ -355,6 +363,7 @@ func (c *containerConfig) hostConfig() *enginecontainer.HostConfig {
 		Mounts:         c.mounts(),
 		ReadonlyRootfs: c.spec().ReadOnly,
 		Isolation:      c.isolation(),
+		Init:           c.init(),
 	}
 
 	if c.spec().DNSConfig != nil {
diff --git a/integration/internal/swarm/service.go b/integration/internal/swarm/service.go
index e6a1bfcdd0..5567ad6ede 100644
--- a/integration/internal/swarm/service.go
+++ b/integration/internal/swarm/service.go
@@ -86,6 +86,14 @@ func defaultServiceSpec() swarmtypes.ServiceSpec {
 	return spec
 }
 
+// ServiceWithInit sets whether the service should use init or not
+func ServiceWithInit(b *bool) func(*swarmtypes.ServiceSpec) {
+	return func(spec *swarmtypes.ServiceSpec) {
+		ensureContainerSpec(spec)
+		spec.TaskTemplate.ContainerSpec.Init = b
+	}
+}
+
 // ServiceWithImage sets the image to use for the service
 func ServiceWithImage(image string) func(*swarmtypes.ServiceSpec) {
 	return func(spec *swarmtypes.ServiceSpec) {
diff --git a/integration/service/create_test.go b/integration/service/create_test.go
index 68af6e825a..517ee0d514 100644
--- a/integration/service/create_test.go
+++ b/integration/service/create_test.go
@@ -2,6 +2,7 @@ package service // import "github.com/docker/docker/integration/service"
 
 import (
 	"context"
+	"fmt"
 	"io/ioutil"
 	"testing"
 	"time"
@@ -11,11 +12,64 @@ import (
 	swarmtypes "github.com/docker/docker/api/types/swarm"
 	"github.com/docker/docker/client"
 	"github.com/docker/docker/integration/internal/swarm"
+	"github.com/docker/docker/internal/test/daemon"
 	"github.com/gotestyourself/gotestyourself/assert"
 	is "github.com/gotestyourself/gotestyourself/assert/cmp"
 	"github.com/gotestyourself/gotestyourself/poll"
 )
 
+func TestServiceCreateInit(t *testing.T) {
+	defer setupTest(t)()
+	t.Run("daemonInitDisabled", testServiceCreateInit(false))
+	t.Run("daemonInitEnabled", testServiceCreateInit(true))
+}
+
+func testServiceCreateInit(daemonEnabled bool) func(t *testing.T) {
+	return func(t *testing.T) {
+		var ops = []func(*daemon.Daemon){}
+
+		if daemonEnabled {
+			ops = append(ops, daemon.WithInit)
+		}
+		d := swarm.NewSwarm(t, testEnv, ops...)
+		defer d.Stop(t)
+		client := d.NewClientT(t)
+		defer client.Close()
+
+		booleanTrue := true
+		booleanFalse := false
+
+		serviceID := swarm.CreateService(t, d)
+		poll.WaitOn(t, serviceRunningTasksCount(client, serviceID, 1), swarm.ServicePoll)
+		i := inspectServiceContainer(t, client, serviceID)
+		// HostConfig.Init == nil means that it delegates to daemon configuration
+		assert.Check(t, i.HostConfig.Init == nil)
+
+		serviceID = swarm.CreateService(t, d, swarm.ServiceWithInit(&booleanTrue))
+		poll.WaitOn(t, serviceRunningTasksCount(client, serviceID, 1), swarm.ServicePoll)
+		i = inspectServiceContainer(t, client, serviceID)
+		assert.Check(t, is.Equal(true, *i.HostConfig.Init))
+
+		serviceID = swarm.CreateService(t, d, swarm.ServiceWithInit(&booleanFalse))
+		poll.WaitOn(t, serviceRunningTasksCount(client, serviceID, 1), swarm.ServicePoll)
+		i = inspectServiceContainer(t, client, serviceID)
+		assert.Check(t, is.Equal(false, *i.HostConfig.Init))
+	}
+}
+
+func inspectServiceContainer(t *testing.T, client client.APIClient, serviceID string) types.ContainerJSON {
+	t.Helper()
+	filter := filters.NewArgs()
+	filter.Add("label", fmt.Sprintf("com.docker.swarm.service.id=%s", serviceID))
+	containers, err := client.ContainerList(context.Background(), types.ContainerListOptions{Filters: filter})
+	assert.NilError(t, err)
+	assert.Check(t, is.Len(containers, 1))
+
+	i, err := client.ContainerInspect(context.Background(), containers[0].ID)
+	assert.NilError(t, err)
+	return i
+}
+
 func TestCreateServiceMultipleTimes(t *testing.T) {
 	defer setupTest(t)()
 	d := swarm.NewSwarm(t, testEnv)
diff --git a/internal/test/daemon/daemon.go b/internal/test/daemon/daemon.go
index 9ba13edc0a..a0d7ed4855 100644
--- a/internal/test/daemon/daemon.go
+++ b/internal/test/daemon/daemon.go
@@ -66,6 +66,7 @@ type Daemon struct {
 	userlandProxy bool
 	execRoot      string
 	experimental  bool
+	init          bool
 	dockerdBinary string
 	log           logT
 
@@ -229,7 +230,10 @@ func (d *Daemon) StartWithLogFile(out *os.File, providedArgs ...string) error {
 		fmt.Sprintf("--userland-proxy=%t", d.userlandProxy),
 	)
 	if d.experimental {
-		args = append(args, "--experimental", "--init")
+		args = append(args, "--experimental")
+	}
+	if d.init {
+		args = append(args, "--init")
 	}
 	if !(d.UseDefaultHost || d.UseDefaultTLSHost) {
 		args = append(args, []string{"--host", d.Sock()}...)
diff --git a/internal/test/daemon/ops.go b/internal/test/daemon/ops.go
index 288fe88070..34db073b57 100644
--- a/internal/test/daemon/ops.go
+++ b/internal/test/daemon/ops.go
@@ -5,6 +5,12 @@ import "github.com/docker/docker/internal/test/environment"
 // WithExperimental sets the daemon in experimental mode
 func WithExperimental(d *Daemon) {
 	d.experimental = true
+	d.init = true
+}
+
+// WithInit sets the daemon init
+func WithInit(d *Daemon) {
+	d.init = true
 }
 
 // WithDockerdBinary sets the dockerd binary to the specified one