Add systctl support for services

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn 2019-02-12 16:07:07 +01:00
parent a4a50de4b8
commit f620349837
No known key found for this signature in database
GPG Key ID: 76698F39D527CE8C
15 changed files with 229 additions and 4 deletions

View File

@ -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"})
flags.Var(cliopts.NewListOptsRef(&opts.resources.resGenericResources, ValidateSingleGenericResource), "generic-resource", "User defined resources")
flags.SetAnnotation(flagHostAdd, "version", []string{"1.32"})

View File

@ -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 }}
@ -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
}

View File

@ -490,6 +490,7 @@ type serviceOptions struct {
dnsSearch opts.ListOpts
dnsOption opts.ListOpts
hosts opts.ListOpts
sysctls opts.ListOpts
resources resourceOptions
stopGrace opts.DurationOpt
@ -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),
}
}
@ -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,
@ -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"

View File

@ -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))
}

View File

@ -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")
@ -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)
@ -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{}

View File

@ -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)
}
})
}
}

View File

@ -151,6 +151,7 @@ func Service(
Privileges: &privileges,
Isolation: container.Isolation(service.Isolation),
Init: service.Init,
Sysctls: service.Sysctls,
},
LogDriver: logDriver,
Resources: resources,

View File

@ -240,6 +240,10 @@ services:
stop_signal: SIGUSR1
sysctls:
net.core.somaxconn: 1024
net.ipv4.tcp_syncookies: 0
# String or list
# tmpfs: /run
tmpfs:

View File

@ -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,
@ -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
@ -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"

View File

@ -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),

View File

@ -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",

View File

@ -24,7 +24,6 @@ var UnsupportedProperties = []string{
"restart",
"security_opt",
"shm_size",
"sysctls",
"ulimits",
"userns_mode",
}
@ -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"`
@ -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

View File

@ -3696,6 +3696,7 @@ _docker_service_update_and_create() {
--placement-pref
--publish -p
--secret
--sysctl
"
case "$prev" in
@ -3746,6 +3747,8 @@ _docker_service_update_and_create() {
--rollback
--secret-add
--secret-rm
--sysctl-add
--sysctl-rm
"
boolean_options="$boolean_options

View File

@ -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")

View File

@ -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")