Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add systctl support for services #1754

Merged
merged 1 commit into from
Mar 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cli/command/service/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ func newCreateCommand(dockerCli command.Cli) *cobra.Command {
flags.SetAnnotation(flagHost, "version", []string{"1.25"})
flags.BoolVar(&opts.init, flagInit, false, "Use an init inside each service container to forward signals and reap processes")
flags.SetAnnotation(flagInit, "version", []string{"1.37"})
flags.Var(&opts.sysctls, flagSysCtl, "Sysctl options")
flags.SetAnnotation(flagSysCtl, "version", []string{"1.40"})

thaJeztah marked this conversation as resolved.
Show resolved Hide resolved
flags.Var(cliopts.NewListOptsRef(&opts.resources.resGenericResources, ValidateSingleGenericResource), "generic-resource", "User defined resources")
flags.SetAnnotation(flagHostAdd, "version", []string{"1.32"})
Expand Down
13 changes: 13 additions & 0 deletions cli/command/service/formatter.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ ContainerSpec:
{{- if .ContainerUser }}
User: {{ .ContainerUser }}
{{- end }}
{{- if .ContainerSysCtls }}
SysCtls:
{{- range $k, $v := .ContainerSysCtls }}
{{ $k }}{{if $v }}: {{ $v }}{{ end }}
{{- end }}{{ end }}
{{- if .ContainerMounts }}
Mounts:
{{- end }}
Expand Down Expand Up @@ -415,6 +420,14 @@ func (ctx *serviceInspectContext) ContainerMounts() []mounttypes.Mount {
return ctx.Service.Spec.TaskTemplate.ContainerSpec.Mounts
}

func (ctx *serviceInspectContext) ContainerSysCtls() map[string]string {
return ctx.Service.Spec.TaskTemplate.ContainerSpec.Sysctls
}

func (ctx *serviceInspectContext) HasContainerSysCtls() bool {
return len(ctx.Service.Spec.TaskTemplate.ContainerSpec.Sysctls) > 0
}

func (ctx *serviceInspectContext) HasResources() bool {
return ctx.Service.Spec.TaskTemplate.Resources != nil
}
Expand Down
6 changes: 6 additions & 0 deletions cli/command/service/opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,7 @@ type serviceOptions struct {
dnsSearch opts.ListOpts
dnsOption opts.ListOpts
hosts opts.ListOpts
sysctls opts.ListOpts

resources resourceOptions
stopGrace opts.DurationOpt
Expand Down Expand Up @@ -531,6 +532,7 @@ func newServiceOptions() *serviceOptions {
dnsOption: opts.NewListOpts(nil),
dnsSearch: opts.NewListOpts(opts.ValidateDNSSearch),
hosts: opts.NewListOpts(opts.ValidateExtraHost),
sysctls: opts.NewListOpts(nil),
}
}

Expand Down Expand Up @@ -643,6 +645,7 @@ func (options *serviceOptions) ToService(ctx context.Context, apiClient client.N
StopGracePeriod: options.ToStopGracePeriod(flags),
Healthcheck: healthConfig,
Isolation: container.Isolation(options.isolation),
Sysctls: opts.ConvertKVStringsToMap(options.sysctls.GetAll()),
},
Networks: networks,
Resources: resources,
Expand Down Expand Up @@ -890,6 +893,9 @@ const (
flagRollbackOrder = "rollback-order"
flagRollbackParallelism = "rollback-parallelism"
flagInit = "init"
flagSysCtl = "sysctl"
flagSysCtlAdd = "sysctl-add"
flagSysCtlRemove = "sysctl-rm"
flagStopGracePeriod = "stop-grace-period"
flagStopSignal = "stop-signal"
flagTTY = "tty"
Expand Down
13 changes: 13 additions & 0 deletions cli/command/service/opts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,3 +233,16 @@ func TestToServiceMaxReplicasGlobalModeConflict(t *testing.T) {
_, err := opt.ToServiceMode()
assert.Error(t, err, "replicas-max-per-node can only be used with replicated mode")
}

func TestToServiceSysCtls(t *testing.T) {
o := newServiceOptions()
o.mode = "replicated"
o.sysctls.Set("net.ipv4.ip_forward=1")
o.sysctls.Set("kernel.shmmax=123456")

expected := map[string]string{"net.ipv4.ip_forward": "1", "kernel.shmmax": "123456"}
flags := newCreateCommand(nil).Flags()
service, err := o.ToService(context.Background(), &fakeClient{}, flags)
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(service.TaskTemplate.ContainerSpec.Sysctls, expected))
}
25 changes: 25 additions & 0 deletions cli/command/service/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ func newUpdateCommand(dockerCli command.Cli) *cobra.Command {
flags.SetAnnotation(flagHostAdd, "version", []string{"1.25"})
flags.BoolVar(&options.init, flagInit, false, "Use an init inside each service container to forward signals and reap processes")
flags.SetAnnotation(flagInit, "version", []string{"1.37"})
flags.Var(&options.sysctls, flagSysCtlAdd, "Add or update a Sysctl option")
flags.SetAnnotation(flagSysCtlAdd, "version", []string{"1.40"})
flags.Var(newListOptsVar(), flagSysCtlRemove, "Remove a Sysctl option")
flags.SetAnnotation(flagSysCtlRemove, "version", []string{"1.40"})

// Add needs parsing, Remove only needs the key
flags.Var(newListOptsVar(), flagGenericResourcesRemove, "Remove a Generic resource")
Expand Down Expand Up @@ -328,6 +332,8 @@ func updateService(ctx context.Context, apiClient client.NetworkAPIClient, flags
return err
}

updateSysCtls(flags, &task.ContainerSpec.Sysctls)

if anyChanged(flags, flagLimitCPU, flagLimitMemory) {
taskResources().Limits = spec.TaskTemplate.Resources.Limits
updateInt64Value(flagLimitCPU, &task.Resources.Limits.NanoCPUs)
Expand Down Expand Up @@ -661,6 +667,25 @@ func updateLabels(flags *pflag.FlagSet, field *map[string]string) {
}
}

func updateSysCtls(flags *pflag.FlagSet, field *map[string]string) {
if *field != nil && flags.Changed(flagSysCtlRemove) {
values := flags.Lookup(flagSysCtlRemove).Value.(*opts.ListOpts).GetAll()
for key := range opts.ConvertKVStringsToMap(values) {
delete(*field, key)
}
}
if flags.Changed(flagSysCtlAdd) {
if *field == nil {
*field = map[string]string{}
}

values := flags.Lookup(flagSysCtlAdd).Value.(*opts.ListOpts).GetAll()
for key, value := range opts.ConvertKVStringsToMap(values) {
(*field)[key] = value
}
}
}

func updateEnvironment(flags *pflag.FlagSet, field *[]string) {
if flags.Changed(flagEnvAdd) {
envSet := map[string]string{}
Expand Down
97 changes: 97 additions & 0 deletions cli/command/service/update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -828,3 +828,100 @@ func TestUpdateMaxReplicas(t *testing.T) {

assert.DeepEqual(t, svc.TaskTemplate.Placement, &swarm.Placement{MaxReplicas: uint64(2)})
}

func TestUpdateSysCtls(t *testing.T) {
ctx := context.Background()

tests := []struct {
name string
spec map[string]string
add []string
rm []string
expected map[string]string
}{
{
name: "from scratch",
add: []string{"sysctl.zet=value-99", "sysctl.alpha=value-1"},
expected: map[string]string{"sysctl.zet": "value-99", "sysctl.alpha": "value-1"},
},
{
name: "append new",
spec: map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2"},
add: []string{"new.sysctl=newvalue"},
expected: map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2", "new.sysctl": "newvalue"},
},
{
name: "append duplicate is a no-op",
spec: map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2"},
add: []string{"sysctl.one=value-1"},
expected: map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2"},
},
{
name: "remove and append existing is a no-op",
spec: map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2"},
add: []string{"sysctl.one=value-1"},
rm: []string{"sysctl.one=value-1"},
expected: map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2"},
},
{
name: "remove and append new should append",
spec: map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2"},
add: []string{"new.sysctl=newvalue"},
rm: []string{"new.sysctl=newvalue"},
expected: map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2", "new.sysctl": "newvalue"},
},
{
name: "update existing",
spec: map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2"},
add: []string{"sysctl.one=newvalue"},
expected: map[string]string{"sysctl.one": "newvalue", "sysctl.two": "value-2"},
},
{
name: "update existing twice",
spec: map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2"},
add: []string{"sysctl.one=newvalue", "sysctl.one=evennewervalue"},
expected: map[string]string{"sysctl.one": "evennewervalue", "sysctl.two": "value-2"},
},
{
name: "remove all",
spec: map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2"},
rm: []string{"sysctl.one=value-1", "sysctl.two=value-2"},
expected: map[string]string{},
},
{
name: "remove by key",
spec: map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2"},
rm: []string{"sysctl.one"},
expected: map[string]string{"sysctl.two": "value-2"},
},
{
name: "remove by key and different value",
spec: map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2"},
rm: []string{"sysctl.one=anyvalueyoulike"},
expected: map[string]string{"sysctl.two": "value-2"},
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
svc := swarm.ServiceSpec{
TaskTemplate: swarm.TaskSpec{
ContainerSpec: &swarm.ContainerSpec{Sysctls: tc.spec},
},
}
flags := newUpdateCommand(nil).Flags()
for _, v := range tc.add {
assert.NilError(t, flags.Set(flagSysCtlAdd, v))
}
for _, v := range tc.rm {
assert.NilError(t, flags.Set(flagSysCtlRemove, v))
}
err := updateService(ctx, &fakeClient{}, flags, &svc)
assert.NilError(t, err)
if !assert.Check(t, is.DeepEqual(svc.TaskTemplate.ContainerSpec.Sysctls, tc.expected)) {
t.Logf("expected: %v", tc.expected)
t.Logf("actual: %v", svc.TaskTemplate.ContainerSpec.Sysctls)
}
})
}
}
1 change: 1 addition & 0 deletions cli/compose/convert/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ func Service(
Privileges: &privileges,
Isolation: container.Isolation(service.Isolation),
Init: service.Init,
Sysctls: service.Sysctls,
},
LogDriver: logDriver,
Resources: resources,
Expand Down
4 changes: 4 additions & 0 deletions cli/compose/loader/full-example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,10 @@ services:

stop_signal: SIGUSR1

sysctls:
net.core.somaxconn: 1024
net.ipv4.tcp_syncookies: 0

# String or list
# tmpfs: /run
tmpfs:
Expand Down
15 changes: 13 additions & 2 deletions cli/compose/loader/full-struct_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -346,8 +346,12 @@ func services(workingDir, homeDir string) []types.ServiceConfig {
StdinOpen: true,
StopSignal: "SIGUSR1",
StopGracePeriod: durationPtr(20 * time.Second),
Tmpfs: []string{"/run", "/tmp"},
Tty: true,
Sysctls: map[string]string{
"net.core.somaxconn": "1024",
"net.ipv4.tcp_syncookies": "0",
},
Tmpfs: []string{"/run", "/tmp"},
Tty: true,
Ulimits: map[string]*types.UlimitsConfig{
"nproc": {
Single: 65535,
Expand Down Expand Up @@ -756,6 +760,9 @@ services:
stdin_open: true
stop_grace_period: 20s
stop_signal: SIGUSR1
sysctls:
net.core.somaxconn: "1024"
net.ipv4.tcp_syncookies: "0"
tmpfs:
- /run
- /tmp
Expand Down Expand Up @@ -1325,6 +1332,10 @@ func fullExampleJSON(workingDir string) string {
"stdin_open": true,
"stop_grace_period": "20s",
"stop_signal": "SIGUSR1",
"sysctls": {
"net.core.somaxconn": "1024",
"net.ipv4.tcp_syncookies": "0"
},
"tmpfs": [
"/run",
"/tmp"
Expand Down
1 change: 1 addition & 0 deletions cli/compose/loader/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ func createTransformHook(additionalTransformers ...Transformer) mapstructure.Dec
reflect.TypeOf(types.ServiceConfigObjConfig{}): transformStringSourceMap,
reflect.TypeOf(types.StringOrNumberList{}): transformStringOrNumberList,
reflect.TypeOf(map[string]*types.ServiceNetworkConfig{}): transformServiceNetworkMap,
reflect.TypeOf(types.Mapping{}): transformMappingOrListFunc("=", false),
reflect.TypeOf(types.MappingWithEquals{}): transformMappingOrListFunc("=", true),
reflect.TypeOf(types.Labels{}): transformMappingOrListFunc("=", false),
reflect.TypeOf(types.MappingWithColon{}): transformMappingOrListFunc(":", false),
Expand Down
41 changes: 41 additions & 0 deletions cli/compose/loader/loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1461,6 +1461,47 @@ services:
}
}

func TestLoadSysctls(t *testing.T) {
config, err := loadYAML(`
version: "3.8"
services:
web:
image: busybox
sysctls:
- net.core.somaxconn=1024
- net.ipv4.tcp_syncookies=0
- testing.one.one=
- testing.one.two
`)
assert.NilError(t, err)

expected := types.Mapping{
"net.core.somaxconn": "1024",
"net.ipv4.tcp_syncookies": "0",
"testing.one.one": "",
"testing.one.two": "",
}

assert.Assert(t, is.Len(config.Services, 1))
assert.Check(t, is.DeepEqual(expected, config.Services[0].Sysctls))

config, err = loadYAML(`
version: "3.8"
services:
web:
image: busybox
sysctls:
net.core.somaxconn: 1024
net.ipv4.tcp_syncookies: 0
testing.one.one: ""
testing.one.two:
`)
assert.NilError(t, err)

assert.Assert(t, is.Len(config.Services, 1))
assert.Check(t, is.DeepEqual(expected, config.Services[0].Sysctls))
}

func TestTransform(t *testing.T) {
var source = []interface{}{
"80-82:8080-8082",
Expand Down
9 changes: 7 additions & 2 deletions cli/compose/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ var UnsupportedProperties = []string{
"restart",
"security_opt",
"shm_size",
"sysctls",
"ulimits",
"userns_mode",
}
Expand Down Expand Up @@ -200,7 +199,7 @@ type ServiceConfig struct {
StdinOpen bool `mapstructure:"stdin_open" yaml:"stdin_open,omitempty" json:"stdin_open,omitempty"`
StopGracePeriod *Duration `mapstructure:"stop_grace_period" yaml:"stop_grace_period,omitempty" json:"stop_grace_period,omitempty"`
StopSignal string `mapstructure:"stop_signal" yaml:"stop_signal,omitempty" json:"stop_signal,omitempty"`
Sysctls StringList `yaml:",omitempty" json:"sysctls,omitempty"`
Sysctls Mapping `yaml:",omitempty" json:"sysctls,omitempty"`
Tmpfs StringList `yaml:",omitempty" json:"tmpfs,omitempty"`
Tty bool `mapstructure:"tty" yaml:"tty,omitempty" json:"tty,omitempty"`
Ulimits map[string]*UlimitsConfig `yaml:",omitempty" json:"ulimits,omitempty"`
Expand Down Expand Up @@ -240,6 +239,12 @@ type StringOrNumberList []string
// For the key without value (`key`), the mapped value is set to nil.
type MappingWithEquals map[string]*string

// Mapping is a mapping type that can be converted from a list of
// key[=value] strings.
// For the key with an empty value (`key=`), or key without value (`key`), the
// mapped value is set to an empty string `""`.
type Mapping map[string]string

// Labels is a mapping type for labels
type Labels map[string]string

Expand Down
3 changes: 3 additions & 0 deletions contrib/completion/bash/docker
Original file line number Diff line number Diff line change
Expand Up @@ -3696,6 +3696,7 @@ _docker_service_update_and_create() {
--placement-pref
--publish -p
--secret
--sysctl
"

case "$prev" in
Expand Down Expand Up @@ -3746,6 +3747,8 @@ _docker_service_update_and_create() {
--rollback
--secret-add
--secret-rm
--sysctl-add
--sysctl-rm
"

boolean_options="$boolean_options
Expand Down
1 change: 1 addition & 0 deletions docs/reference/commandline/service_create.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ Options:
--secret secret Specify secrets to expose to the service
--stop-grace-period duration Time to wait before force killing a container (ns|us|ms|s|m|h) (default 10s)
--stop-signal string Signal to stop the container
--sysctl list Sysctl options
-t, --tty Allocate a pseudo-TTY
--update-delay duration Delay between updates (ns|us|ms|s|m|h) (default 0s)
--update-failure-action string Action on update failure ("pause"|"continue"|"rollback") (default "pause")
Expand Down
Loading