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