Skip to content

Commit

Permalink
Add systctl support for services
Browse files Browse the repository at this point in the history
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
  • Loading branch information
thaJeztah committed Mar 19, 2019
1 parent bf959a2 commit 6327aa3
Show file tree
Hide file tree
Showing 15 changed files with 222 additions and 4 deletions.
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(flagInit, "version", []string{"1.40"})

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 @@ -95,6 +95,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 @@ -398,6 +403,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
8 changes: 6 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
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
2 changes: 2 additions & 0 deletions docs/reference/commandline/service_update.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ Options:
--secret-rm list Remove a secret
--stop-grace-period duration Time to wait before force killing a container (ns|us|ms|s|m|h)
--stop-signal string Signal to stop the container
--sysctl-add list Add or update a Sysctl option
--sysctl-rm list Remove a Sysctl option
-t, --tty Allocate a pseudo-TTY
--update-delay duration Delay between updates (ns|us|ms|s|m|h)
--update-failure-action string Action on update failure ("pause"|"continue"|"rollback")
Expand Down

0 comments on commit 6327aa3

Please sign in to comment.