daemon: use structured logs for printing reloaded config, move to cli

- Move logging out of config.Reload and daemon.Reload itself, as it was not
  the right place to know whether it was a "signal" that triggered the reload.
- Use Daemon.Config() to get the new config after reloading. This returns an
  immutable copy of the daemon's config, so we can redact fields without having
  to use an ad-hoc struct to shadow the underlying fields.
- Use structured logs for logging config reload events.

Before this (plain text):

    INFO[2025-02-08T12:13:53.389649297Z] Got signal to reload configuration, reloading from: /etc/docker/daemon.json
    INFO[2025-02-08T12:30:34.857691260Z] Reloaded configuration: {"pidfile":"/var/run/docker.pid","data-root":"/var/lib/docker","exec-root":"/var/run/docker","group":"docker","max-concurrent-downloads":3,"max-concurrent-uploads":5,"max-download-attempts":5,"shutdown-timeout":15,"hosts":["unix:///var/run/docker.sock"],"log-level":"info","log-format":"text","swarm-default-advertise-addr":"","swarm-raft-heartbeat-tick":0,"swarm-raft-election-tick":0,"metrics-addr":"","host-gateway-ips":[""],"log-driver":"json-file","mtu":1500,"ip":"0.0.0.0","icc":true,"iptables":true,"ip6tables":true,"ip-forward":true,"ip-masq":true,"userland-proxy":true,"userland-proxy-path":"/usr/local/bin/docker-proxy","default-address-pools":{"Values":null},"network-control-plane-mtu":1500,"experimental":false,"containerd":"/var/run/docker/containerd/containerd.sock","features":{"containerd-snapshotter":false},"builder":{"GC":{},"Entitlements":{}},"containerd-namespace":"moby","containerd-plugin-namespace":"plugins.moby","default-runtime":"runc","runtimes":{"crun":{"path":"/usr/local/bin/crun"}},"seccomp-profile":"builtin","default-shm-size":67108864,"default-ipc-mode":"private","default-cgroupns-mode":"private","resolv-conf":"/etc/resolv.conf","proxies":{}}

Before this (JSON logs):

    {"level":"info","msg":"Reloaded configuration: {\"pidfile\":\"/var/run/docker.pid\",\"data-root\":\"/var/lib/docker\",\"exec-root\":\"/var/run/docker\",\"group\":\"docker\",\"max-concurrent-downloads\":3,\"max-concurrent-uploads\":5,\"max-download-attempts\":5,\"shutdown-timeout\":15,\"hosts\":[\"unix:///var/run/docker.sock\"],\"log-level\":\"info\",\"log-format\":\"json\",\"swarm-default-advertise-addr\":\"\",\"swarm-raft-heartbeat-tick\":0,\"swarm-raft-election-tick\":0,\"metrics-addr\":\"\",\"host-gateway-ips\":[\"\"],\"log-driver\":\"json-file\",\"mtu\":1500,\"ip\":\"0.0.0.0\",\"icc\":true,\"iptables\":true,\"ip6tables\":true,\"ip-forward\":true,\"ip-masq\":true,\"userland-proxy\":true,\"userland-proxy-path\":\"/usr/local/bin/docker-proxy\",\"default-address-pools\":{\"Values\":null},\"network-control-plane-mtu\":1500,\"experimental\":false,\"containerd\":\"/var/run/docker/containerd/containerd.sock\",\"features\":{\"containerd-snapshotter\":false},\"builder\":{\"GC\":{},\"Entitlements\":{}},\"containerd-namespace\":\"moby\",\"containerd-plugin-namespace\":\"plugins.moby\",\"default-runtime\":\"runc\",\"runtimes\":{\"crun\":{\"path\":\"/usr/local/bin/crun\"}},\"seccomp-profile\":\"builtin\",\"default-shm-size\":67108864,\"default-ipc-mode\":\"private\",\"default-cgroupns-mode\":\"private\",\"resolv-conf\":\"/etc/resolv.conf\",\"proxies\":{}}","time":"2025-02-08T12:24:38.600761054Z"}

After this (plain text):

    INFO[2025-02-08T12:30:34.835953594Z] Got signal to reload configuration            config-file=/etc/docker/daemon.json
    INFO[2025-02-08T12:30:34.857614135Z] Reloaded configuration                        config="{\"pidfile\":\"/var/run/docker.pid\",\"data-root\":\"/var/lib/docker\",\"exec-root\":\"/var/run/docker\",\"group\":\"docker\",\"max-concurrent-downloads\":3,\"max-concurrent-uploads\":5,\"max-download-attempts\":5,\"shutdown-timeout\":15,\"hosts\":[\"unix:///var/run/docker.sock\"],\"log-level\":\"info\",\"log-format\":\"text\",\"swarm-default-advertise-addr\":\"\",\"swarm-raft-heartbeat-tick\":0,\"swarm-raft-election-tick\":0,\"metrics-addr\":\"\",\"host-gateway-ips\":[\"\"],\"log-driver\":\"json-file\",\"mtu\":1500,\"ip\":\"0.0.0.0\",\"icc\":true,\"iptables\":true,\"ip6tables\":true,\"ip-forward\":true,\"ip-masq\":true,\"userland-proxy\":true,\"userland-proxy-path\":\"/usr/local/bin/docker-proxy\",\"default-address-pools\":{\"Values\":null},\"network-control-plane-mtu\":1500,\"experimental\":false,\"containerd\":\"/var/run/docker/containerd/containerd.sock\",\"features\":{\"containerd-snapshotter\":false},\"builder\":{\"GC\":{},\"Entitlements\":{}},\"containerd-namespace\":\"moby\",\"containerd-plugin-namespace\":\"plugins.moby\",\"default-runtime\":\"runc\",\"runtimes\":{\"crun\":{\"path\":\"/usr/local/bin/crun\"}},\"seccomp-profile\":\"builtin\",\"default-shm-size\":67108864,\"default-ipc-mode\":\"private\",\"default-cgroupns-mode\":\"private\",\"resolv-conf\":\"/etc/resolv.conf\",\"proxies\":{}}"

After this (JSON logs):

    {"config-file":"/etc/docker/daemon.json","level":"info","msg":"Got signal to reload configuration","time":"2025-02-08T12:24:38.589955637Z"}
    {"config":"{\"pidfile\":\"/var/run/docker.pid\",\"data-root\":\"/var/lib/docker\",\"exec-root\":\"/var/run/docker\",\"group\":\"docker\",\"max-concurrent-downloads\":3,\"max-concurrent-uploads\":5,\"max-download-attempts\":5,\"shutdown-timeout\":15,\"hosts\":[\"unix:///var/run/docker.sock\"],\"log-level\":\"info\",\"log-format\":\"json\",\"swarm-default-advertise-addr\":\"\",\"swarm-raft-heartbeat-tick\":0,\"swarm-raft-election-tick\":0,\"metrics-addr\":\"\",\"host-gateway-ips\":[\"\"],\"log-driver\":\"json-file\",\"mtu\":1500,\"ip\":\"0.0.0.0\",\"icc\":true,\"iptables\":true,\"ip6tables\":true,\"ip-forward\":true,\"ip-masq\":true,\"userland-proxy\":true,\"userland-proxy-path\":\"/usr/local/bin/docker-proxy\",\"default-address-pools\":{\"Values\":null},\"network-control-plane-mtu\":1500,\"experimental\":false,\"containerd\":\"/var/run/docker/containerd/containerd.sock\",\"features\":{\"containerd-snapshotter\":false},\"builder\":{\"GC\":{},\"Entitlements\":{}},\"containerd-namespace\":\"moby\",\"containerd-plugin-namespace\":\"plugins.moby\",\"default-runtime\":\"runc\",\"runtimes\":{\"crun\":{\"path\":\"/usr/local/bin/crun\"}},\"seccomp-profile\":\"builtin\",\"default-shm-size\":67108864,\"default-ipc-mode\":\"private\",\"default-cgroupns-mode\":\"private\",\"resolv-conf\":\"/etc/resolv.conf\",\"proxies\":{}}","level":"info","msg":"Reloaded configuration","time":"2025-02-08T12:24:38.600736179Z"}

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn
2025-02-08 14:25:52 +01:00
parent 40cdab0a3a
commit d1c6550f71
5 changed files with 51 additions and 24 deletions

View File

@@ -3,6 +3,7 @@ package main
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"
"encoding/json"
"fmt" "fmt"
"net" "net"
"net/http" "net/http"
@@ -469,14 +470,15 @@ type builderOptions struct {
func (cli *daemonCLI) reloadConfig() { func (cli *daemonCLI) reloadConfig() {
ctx := context.TODO() ctx := context.TODO()
log.G(ctx).WithField("config-file", *cli.configFile).Info("Got signal to reload configuration")
reload := func(c *config.Config) { reload := func(c *config.Config) {
if err := validateAuthzPlugins(c.AuthorizationPlugins, cli.d.PluginStore); err != nil { if err := validateAuthzPlugins(c.AuthorizationPlugins, cli.d.PluginStore); err != nil {
log.G(ctx).Fatalf("Error validating authorization plugin: %v", err) log.G(ctx).WithError(err).Fatal("Error validating authorization plugin")
return return
} }
if err := cli.d.Reload(c); err != nil { if err := cli.d.Reload(c); err != nil {
log.G(ctx).Errorf("Error reconfiguring the daemon: %v", err) log.G(ctx).WithError(err).Error("Error reconfiguring the daemon")
return return
} }
@@ -497,7 +499,17 @@ func (cli *daemonCLI) reloadConfig() {
} }
if err := config.Reload(*cli.configFile, cli.flags, reload); err != nil { if err := config.Reload(*cli.configFile, cli.flags, reload); err != nil {
log.G(ctx).Error(err) log.G(ctx).WithError(err).Error("Error reloading configuration")
return
}
sanitizedConfig := config.Sanitize(cli.d.Config())
jsonData, err := json.Marshal(sanitizedConfig)
if err != nil {
log.G(context.TODO()).WithError(err).Warn("Error when marshaling configuration for printing")
log.G(context.TODO()).Info("Reloaded configuration")
} else {
log.G(context.TODO()).WithField("config", string(jsonData)).Info("Reloaded configuration")
} }
} }

View File

@@ -2,7 +2,6 @@ package config // import "github.com/docker/docker/daemon/config"
import ( import (
"bytes" "bytes"
"context"
"encoding/json" "encoding/json"
stderrors "errors" stderrors "errors"
"fmt" "fmt"
@@ -380,7 +379,6 @@ func GetConflictFreeLabels(labels []string) ([]string, error) {
// Reload reads the configuration in the host and reloads the daemon and server. // Reload reads the configuration in the host and reloads the daemon and server.
func Reload(configFile string, flags *pflag.FlagSet, reload func(*Config)) error { func Reload(configFile string, flags *pflag.FlagSet, reload func(*Config)) error {
log.G(context.TODO()).Infof("Got signal to reload configuration, reloading from: %s", configFile)
newConfig, err := getConflictFreeConfiguration(configFile, flags) newConfig, err := getConflictFreeConfiguration(configFile, flags)
if err != nil { if err != nil {
if flags.Changed("config-file") || !os.IsNotExist(err) { if flags.Changed("config-file") || !os.IsNotExist(err) {
@@ -801,3 +799,14 @@ func migrateHostGatewayIP(config *Config) {
config.HostGatewayIP = nil //nolint:staticcheck // ignore SA1019: clearing old value. config.HostGatewayIP = nil //nolint:staticcheck // ignore SA1019: clearing old value.
} }
} }
// Sanitize sanitizes the config for printing. It is currently limited to
// masking usernames and passwords from Proxy URLs.
func Sanitize(cfg Config) Config {
cfg.CommonConfig.Proxies = Proxies{
HTTPProxy: MaskCredentials(cfg.HTTPProxy),
HTTPSProxy: MaskCredentials(cfg.HTTPSProxy),
NoProxy: MaskCredentials(cfg.NoProxy),
}
return cfg
}

View File

@@ -794,3 +794,26 @@ func TestMaskURLCredentials(t *testing.T) {
assert.Equal(t, maskedURL, test.maskedURL) assert.Equal(t, maskedURL, test.maskedURL)
} }
} }
func TestSanitize(t *testing.T) {
const (
userPass = "myuser:mypassword@"
proxyRawURL = "https://" + userPass + "example.org"
proxyURL = "https://xxxxx:xxxxx@example.org"
)
sanitizedCfg := Sanitize(Config{
CommonConfig: CommonConfig{
Proxies: Proxies{
HTTPProxy: proxyRawURL,
HTTPSProxy: proxyRawURL,
NoProxy: proxyRawURL,
},
},
})
expectedProxies := Proxies{
HTTPProxy: proxyURL,
HTTPSProxy: proxyURL,
NoProxy: proxyURL,
}
assert.Check(t, is.DeepEqual(sanitizedCfg.Proxies, expectedProxies))
}

View File

@@ -96,6 +96,7 @@ func (daemon *Daemon) Reload(conf *config.Config) error {
var txn reloadTxn var txn reloadTxn
for _, reload := range []func(txn *reloadTxn, newCfg *configStore, conf *config.Config, attributes map[string]string) error{ for _, reload := range []func(txn *reloadTxn, newCfg *configStore, conf *config.Config, attributes map[string]string) error{
// TODO(thaJeztah): most of these are defined as method, but don't use the daemon receiver; consider making them regular functions.
daemon.reloadPlatform, daemon.reloadPlatform,
daemon.reloadDebug, daemon.reloadDebug,
daemon.reloadMaxConcurrentDownloadsAndUploads, daemon.reloadMaxConcurrentDownloadsAndUploads,
@@ -115,24 +116,6 @@ func (daemon *Daemon) Reload(conf *config.Config) error {
} }
} }
redactedConfig := struct {
*config.Config
config.Proxies `json:"proxies"`
}{
Config: &newCfg.Config,
Proxies: config.Proxies{
HTTPProxy: config.MaskCredentials(newCfg.HTTPProxy),
HTTPSProxy: config.MaskCredentials(newCfg.HTTPSProxy),
NoProxy: config.MaskCredentials(newCfg.NoProxy),
},
}
jsonData, err := json.Marshal(&redactedConfig)
if err != nil {
log.G(context.TODO()).WithError(err).Warn("Error when marshaling configuration for printing")
log.G(context.TODO()).Info("Reloaded configuration")
} else {
log.G(context.TODO()).Infof("Reloaded configuration: %s", jsonData)
}
daemon.configStore.Store(newCfg) daemon.configStore.Store(newCfg)
daemon.LogDaemonEventWithAttributes(events.ActionReload, attributes) daemon.LogDaemonEventWithAttributes(events.ActionReload, attributes)
return txn.Commit() return txn.Commit()

View File

@@ -440,7 +440,7 @@ func TestDaemonProxy(t *testing.T) {
err := d.Signal(syscall.SIGHUP) err := d.Signal(syscall.SIGHUP)
assert.NilError(t, err) assert.NilError(t, err)
poll.WaitOn(t, d.PollCheckLogs(ctx, daemon.ScanLogsMatchAll("Reloaded configuration:", proxyURL))) poll.WaitOn(t, d.PollCheckLogs(ctx, daemon.ScanLogsMatchAll("Reloaded configuration", proxyURL)))
ok, logs := d.ScanLogsT(ctx, t, daemon.ScanLogsMatchString(userPass)) ok, logs := d.ScanLogsT(ctx, t, daemon.ScanLogsMatchString(userPass))
assert.Assert(t, !ok, "logs should not contain the non-sanitized proxy URL: %s", logs) assert.Assert(t, !ok, "logs should not contain the non-sanitized proxy URL: %s", logs)