From f4b539c24419f45549595af4b8aeead146795091 Mon Sep 17 00:00:00 2001 From: Steven Li Date: Wed, 21 Oct 2026 23:22:34 -0400 Subject: [PATCH] cli/command/service: keep log driver when updating only --log-opt `docker service update --log-opt` was ignored unless `--log-driver` was also set, even if the service already had a logging driver configured. Keep the service's current logging driver when only `--log-opt` is set, and merge the options provided with the service's existing options. When `--log-driver` is set, the options are still replaced with those provided. If the service has no logging driver configured, `--log-opt` is still ignored when `--log-driver` is not set. Co-authored-by: Claude Opus-5-5 Signed-off-by: Steven Li --- cli/command/service/update.go | 20 ++++- cli/command/service/update_test.go | 89 ++++++++++++++++++++ docs/reference/commandline/service_update.md | 33 +++++++- 3 files changed, 137 insertions(+), 5 deletions(-) diff --git a/cli/command/service/update.go b/cli/command/service/update.go index 2e7a0ee25fce..003d0c43805f 100644 --- a/cli/command/service/update.go +++ b/cli/command/service/update.go @@ -1219,10 +1219,13 @@ func updateHosts(flags *pflag.FlagSet, hosts *[]string) error { return nil } -// updateLogDriver updates the log driver only if the log driver flag is set. -// All options will be replaced with those provided on the command line. +// updateLogDriver updates the log driver only if the log driver or log options +// flags are set. If the log driver flag is set, all options will be replaced +// with those provided on the command line. If only log options are set, the +// service's current log driver is kept, and the options provided are merged +// with its existing options. func updateLogDriver(flags *pflag.FlagSet, taskTemplate *swarm.TaskSpec) error { - if !flags.Changed(flagLogDriver) { + if !anyChanged(flags, flagLogDriver, flagLogOpt) { return nil } @@ -1230,6 +1233,15 @@ func updateLogDriver(flags *pflag.FlagSet, taskTemplate *swarm.TaskSpec) error { if err != nil { return err } + options := opts.ConvertKVStringsToMap(flags.Lookup(flagLogOpt).Value.(*opts.ListOpts).GetSlice()) + + if !flags.Changed(flagLogDriver) && taskTemplate.LogDriver != nil { + name = taskTemplate.LogDriver.Name + merged := make(map[string]string, len(taskTemplate.LogDriver.Options)+len(options)) + maps.Copy(merged, taskTemplate.LogDriver.Options) + maps.Copy(merged, options) + options = merged + } if name == "" { return nil @@ -1237,7 +1249,7 @@ func updateLogDriver(flags *pflag.FlagSet, taskTemplate *swarm.TaskSpec) error { taskTemplate.LogDriver = &swarm.Driver{ Name: name, - Options: opts.ConvertKVStringsToMap(flags.Lookup(flagLogOpt).Value.(*opts.ListOpts).GetSlice()), + Options: options, } return nil diff --git a/cli/command/service/update_test.go b/cli/command/service/update_test.go index 51ba0eaec24f..f7f92397164d 100644 --- a/cli/command/service/update_test.go +++ b/cli/command/service/update_test.go @@ -3,6 +3,7 @@ package service import ( "context" "fmt" + "maps" "net/netip" "slices" "strconv" @@ -333,6 +334,94 @@ func TestUpdatePortsDuplicate(t *testing.T) { assert.Check(t, is.Equal(uint32(80), portConfigs[0].TargetPort)) } +func TestUpdateLogDriver(t *testing.T) { + tests := []struct { + name string + flags [][2]string + initial *swarm.Driver + expected *swarm.Driver + }{ + { + name: "no changes", + initial: &swarm.Driver{Name: "json-file", Options: map[string]string{"max-file": "5"}}, + expected: &swarm.Driver{Name: "json-file", Options: map[string]string{"max-file": "5"}}, + }, + { + name: "set driver and options", + flags: [][2]string{{flagLogDriver, "local"}, {flagLogOpt, "max-file=15"}}, + initial: &swarm.Driver{Name: "json-file", Options: map[string]string{"max-file": "5"}}, + expected: &swarm.Driver{Name: "local", Options: map[string]string{"max-file": "15"}}, + }, + { + name: "set driver without options", + flags: [][2]string{{flagLogDriver, "local"}}, + initial: &swarm.Driver{Name: "json-file", Options: map[string]string{"max-file": "5"}}, + expected: &swarm.Driver{Name: "local", Options: map[string]string{}}, + }, + { + name: "set empty driver", + flags: [][2]string{{flagLogDriver, ""}, {flagLogOpt, "max-file=15"}}, + initial: &swarm.Driver{Name: "json-file", Options: map[string]string{"max-file": "5"}}, + expected: &swarm.Driver{Name: "json-file", Options: map[string]string{"max-file": "5"}}, + }, + { + name: "set options with existing driver", + flags: [][2]string{{flagLogOpt, "max-file=15"}, {flagLogOpt, "max-size=10m"}}, + initial: &swarm.Driver{Name: "json-file", Options: map[string]string{"max-file": "5"}}, + expected: &swarm.Driver{Name: "json-file", Options: map[string]string{"max-file": "15", "max-size": "10m"}}, + }, + { + name: "set options merges with existing options", + flags: [][2]string{{flagLogOpt, "max-size=10m"}}, + initial: &swarm.Driver{Name: "json-file", Options: map[string]string{"max-file": "5"}}, + expected: &swarm.Driver{Name: "json-file", Options: map[string]string{"max-file": "5", "max-size": "10m"}}, + }, + { + name: "set options overrides existing option", + flags: [][2]string{{flagLogOpt, "max-file=3"}}, + initial: &swarm.Driver{Name: "json-file", Options: map[string]string{"max-file": "5", "max-size": "10m"}}, + expected: &swarm.Driver{Name: "json-file", Options: map[string]string{"max-file": "3", "max-size": "10m"}}, + }, + { + name: "set options with existing driver without options", + flags: [][2]string{{flagLogOpt, "max-file=15"}}, + initial: &swarm.Driver{Name: "json-file"}, + expected: &swarm.Driver{Name: "json-file", Options: map[string]string{"max-file": "15"}}, + }, + { + name: "set driver and options replaces existing options", + flags: [][2]string{{flagLogDriver, "json-file"}, {flagLogOpt, "max-size=10m"}}, + initial: &swarm.Driver{Name: "json-file", Options: map[string]string{"max-file": "5"}}, + expected: &swarm.Driver{Name: "json-file", Options: map[string]string{"max-size": "10m"}}, + }, + { + // Options are not applied if no log driver is set on the service, + // as they would be applied to the daemon's default log driver, + // which may differ between nodes. + name: "set options without existing driver", + flags: [][2]string{{flagLogOpt, "max-file=15"}}, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + flags := newUpdateCommand(nil).Flags() + for _, flag := range tc.flags { + assert.NilError(t, flags.Set(flag[0], flag[1])) + } + var initial *swarm.Driver + if tc.initial != nil { + initial = &swarm.Driver{Name: tc.initial.Name, Options: maps.Clone(tc.initial.Options)} + } + taskTemplate := &swarm.TaskSpec{LogDriver: tc.initial} + err := updateLogDriver(flags, taskTemplate) + assert.NilError(t, err) + assert.Check(t, is.DeepEqual(taskTemplate.LogDriver, tc.expected)) + // The existing log driver must not be modified in place. + assert.Check(t, is.DeepEqual(tc.initial, initial)) + }) + } +} + func TestUpdateHealthcheckTable(t *testing.T) { type test struct { flags [][2]string diff --git a/docs/reference/commandline/service_update.md b/docs/reference/commandline/service_update.md index 64f81b07e80f..d7ae7a139450 100644 --- a/docs/reference/commandline/service_update.md +++ b/docs/reference/commandline/service_update.md @@ -51,7 +51,7 @@ Update a service | `--limit-memory` | `bytes` | `0` | Limit Memory | | `--limit-pids` | `int64` | `0` | Limit maximum number of processes (default 0 = unlimited) | | `--log-driver` | `string` | | Logging driver for service | -| `--log-opt` | `list` | | Logging driver options | +| [`--log-opt`](#log-opt) | `list` | | Logging driver options | | `--max-concurrent` | `uint` | | Number of job tasks to run concurrently (default equal to --replicas) | | `--memory-swap` | `bytes` | `0` | Swap Bytes (-1 for unlimited) | | `--memory-swappiness` | `int64` | `-1` | Tune memory swappiness (0-100), -1 to reset to default | @@ -211,6 +211,37 @@ $ docker service update \ myservice ``` +### Update logging driver options (--log-driver, --log-opt) + +Use the `--log-opt` flag to update the options of the logging driver that is +configured for a service. If `--log-driver` is not set, the service keeps its +current logging driver, and the options that you set are merged with the +options that are already set for the service. If the service has no logging +driver configured, `--log-opt` has no effect unless `--log-driver` is also set. + +The following example sets the `max-size` option for a service that uses the +`json-file` logging driver, and keeps its existing `max-file` option: + +```console +$ docker service create --log-driver=json-file --log-opt max-file=5 --name myservice nginx:alpine + +$ docker service update --log-opt max-size=10m myservice + +$ docker service inspect --format='{{json .Spec.TaskTemplate.LogDriver}}' myservice +{"Name":"json-file","Options":{"max-file":"5","max-size":"10m"}} +``` + +If you set `--log-driver`, all existing options are replaced with the options +that you set. To remove an option, set the current logging driver again +together with the options that you want to keep: + +```console +$ docker service update --log-driver=json-file --log-opt max-size=10m myservice + +$ docker service inspect --format='{{json .Spec.TaskTemplate.LogDriver}}' myservice +{"Name":"json-file","Options":{"max-size":"10m"}} +``` + ### Roll back to the previous version of a service (--rollback) Use the `--rollback` option to roll back to the previous version of the service.