| package daemon | |
| import ( | |
| "context" | |
| "fmt" | |
| "os" | |
| "runtime" | |
| "strconv" | |
| "strings" | |
| "sync" | |
| "time" | |
| "github.com/containerd/containerd/v2/pkg/tracing" | |
| "github.com/containerd/log" | |
| "github.com/moby/moby/api/types/system" | |
| "github.com/moby/moby/v2/daemon/command/debug" | |
| "github.com/moby/moby/v2/daemon/config" | |
| "github.com/moby/moby/v2/daemon/internal/filedescriptors" | |
| "github.com/moby/moby/v2/daemon/internal/metrics" | |
| "github.com/moby/moby/v2/daemon/internal/mod" | |
| "github.com/moby/moby/v2/daemon/internal/platform" | |
| "github.com/moby/moby/v2/daemon/logger" | |
| "github.com/moby/moby/v2/daemon/pkg/registry" | |
| "github.com/moby/moby/v2/dockerversion" | |
| "github.com/moby/moby/v2/pkg/meminfo" | |
| "github.com/moby/moby/v2/pkg/parsers/kernel" | |
| "github.com/moby/moby/v2/pkg/parsers/operatingsystem" | |
| "github.com/moby/moby/v2/pkg/sysinfo" | |
| "github.com/opencontainers/selinux/go-selinux" | |
| ) | |
| func doWithTrace[T any](ctx context.Context, name string, f func() T) T { | |
| _, span := tracing.StartSpan(ctx, name) | |
| defer span.End() | |
| return f() | |
| } | |
| // SystemInfo returns information about the host server the daemon is running on. | |
| // | |
| // The only error this should return is due to context cancellation/deadline. | |
| // Anything else should be logged and ignored because this is looking up | |
| // multiple things and is often used for debugging. | |
| // The only case valid early return is when the caller doesn't want the result anymore (ie context cancelled). | |
| func (daemon *Daemon) SystemInfo(ctx context.Context) (*system.Info, error) { | |
| defer metrics.StartTimer(metrics.HostInfoFunctions.WithValues("system_info"))() | |
| sysInfo := daemon.RawSysInfo() | |
| cfg := daemon.config() | |
| v := &system.Info{ | |
| ID: daemon.id, | |
| Images: daemon.imageService.CountImages(ctx), | |
| IPv4Forwarding: !sysInfo.IPv4ForwardingDisabled, | |
| Name: hostName(ctx), | |
| SystemTime: time.Now().Format(time.RFC3339Nano), | |
| LoggingDriver: daemon.defaultLogConfig.Type, | |
| KernelVersion: kernelVersion(ctx), | |
| OperatingSystem: operatingSystem(ctx), | |
| OSVersion: osVersion(ctx), | |
| IndexServerAddress: registry.IndexServer, | |
| OSType: runtime.GOOS, | |
| Architecture: platform.Architecture(), | |
| RegistryConfig: doWithTrace(ctx, "registry.ServiceConfig", daemon.registryService.ServiceConfig), | |
| NCPU: doWithTrace(ctx, "runtime.NumCPU", runtime.NumCPU), | |
| MemTotal: memInfo(ctx).MemTotal, | |
| GenericResources: daemon.genericResources, | |
| DockerRootDir: cfg.Root, | |
| Labels: cfg.Labels, | |
| ExperimentalBuild: cfg.Experimental, | |
| ServerVersion: dockerversion.Version, | |
| HTTPProxy: config.MaskCredentials(getConfigOrEnv(cfg.HTTPProxy, "HTTP_PROXY", "http_proxy")), | |
| HTTPSProxy: config.MaskCredentials(getConfigOrEnv(cfg.HTTPSProxy, "HTTPS_PROXY", "https_proxy")), | |
| NoProxy: getConfigOrEnv(cfg.NoProxy, "NO_PROXY", "no_proxy"), | |
| LiveRestoreEnabled: cfg.LiveRestoreEnabled, | |
| Isolation: daemon.defaultIsolation, | |
| CDISpecDirs: promoteNil(cfg.CDISpecDirs), | |
| NRI: daemon.nri.GetInfo(), | |
| } | |
| daemon.fillContainerStates(v) | |
| daemon.fillDebugInfo(ctx, v) | |
| daemon.fillContainerdInfo(v, &cfg.Config) | |
| daemon.fillAPIInfo(v, &cfg.Config) | |
| // Retrieve platform specific info | |
| if err := daemon.fillPlatformInfo(ctx, v, sysInfo, cfg); err != nil { | |
| return nil, err | |
| } | |
| daemon.fillDriverInfo(v) | |
| daemon.fillPluginsInfo(ctx, v, &cfg.Config) | |
| daemon.fillSecurityOptions(v, sysInfo, &cfg.Config) | |
| daemon.fillLicense(v) | |
| daemon.fillDefaultAddressPools(ctx, v, &cfg.Config) | |
| daemon.fillFirewallInfo(v) | |
| daemon.fillDiscoveredDevicesFromDrivers(ctx, v, &cfg.Config) | |
| return v, nil | |
| } | |
| const ( | |
| moduleName = "github.com/moby/moby/v2" | |
| defaultModuleVersion = "v2.0.0+unknown" | |
| ) | |
| var moduleVersion = sync.OnceValue(func() string { | |
| if v := mod.Version(moduleName); v != "" { | |
| return v | |
| } | |
| return defaultModuleVersion | |
| }) | |
| // SystemVersion returns version information about the daemon. | |
| // | |
| // The only error this should return is due to context cancellation/deadline. | |
| // Anything else should be logged and ignored because this is looking up | |
| // multiple things and is often used for debugging. | |
| // The only case valid early return is when the caller doesn't want the result anymore (ie context cancelled). | |
| func (daemon *Daemon) SystemVersion(ctx context.Context) (system.VersionResponse, error) { | |
| defer metrics.StartTimer(metrics.HostInfoFunctions.WithValues("system_version"))() | |
| kernelVer := kernelVersion(ctx) | |
| cfg := daemon.config() | |
| v := system.VersionResponse{ | |
| Platform: system.PlatformInfo{ | |
| Name: dockerversion.PlatformName, | |
| }, | |
| Version: dockerversion.Version, | |
| APIVersion: config.MaxAPIVersion, | |
| MinAPIVersion: cfg.MinAPIVersion, | |
| Os: runtime.GOOS, | |
| Arch: runtime.GOARCH, | |
| Components: []system.ComponentVersion{ | |
| { | |
| Name: "Engine", | |
| Version: dockerversion.Version, | |
| Details: map[string]string{ | |
| "GitCommit": dockerversion.GitCommit, | |
| "ApiVersion": config.MaxAPIVersion, | |
| "MinAPIVersion": cfg.MinAPIVersion, | |
| "GoVersion": runtime.Version(), | |
| "Os": runtime.GOOS, | |
| "Arch": runtime.GOARCH, | |
| "BuildTime": dockerversion.BuildTime, | |
| "KernelVersion": kernelVer, | |
| "Module": moduleName, | |
| "ModuleVersion": moduleVersion(), | |
| "Experimental": strconv.FormatBool(cfg.Experimental), | |
| }, | |
| }, | |
| }, | |
| // Populate deprecated fields for older clients. | |
| // | |
| // These fields were (soft) deprecated in API v1.35 in favor of the | |
| // per-component build-info, but kept for backward-compatibility, see: | |
| // | |
| // - https://github.com/moby/moby/pull/35705 | |
| // - https://github.com/moby/moby/pull/51359 | |
| GitCommit: dockerversion.GitCommit, | |
| GoVersion: runtime.Version(), | |
| KernelVersion: kernelVer, | |
| Experimental: cfg.Experimental, | |
| BuildTime: dockerversion.BuildTime, | |
| } | |
| if err := daemon.fillPlatformVersion(ctx, &v, cfg); err != nil { | |
| return v, err | |
| } | |
| return v, nil | |
| } | |
| func (daemon *Daemon) fillDriverInfo(v *system.Info) { | |
| v.Driver = daemon.imageService.StorageDriver() | |
| v.DriverStatus = daemon.imageService.LayerStoreStatus() | |
| fillDriverWarnings(v) | |
| } | |
| func (daemon *Daemon) fillPluginsInfo(ctx context.Context, v *system.Info, cfg *config.Config) { | |
| v.Plugins = system.PluginsInfo{ | |
| Volume: daemon.volumes.GetDriverList(), | |
| Network: daemon.GetNetworkDriverList(ctx), | |
| // The authorization plugins are returned in the order they are | |
| // used as they constitute a request/response modification chain. | |
| Authorization: cfg.AuthorizationPlugins, | |
| Log: logger.ListDrivers(), | |
| } | |
| } | |
| // fillSecurityOptions fills the [system.Info.SecurityOptions] field based | |
| // on the daemon configuration. | |
| // | |
| // TODO(thaJeztah): consider making [system.Info.SecurityOptions] a structured response as originally intended in https://github.com/moby/moby/pull/26276 | |
| func (daemon *Daemon) fillSecurityOptions(v *system.Info, sysInfo *sysinfo.SysInfo, cfg *config.Config) { | |
| var securityOptions []string | |
| if sysInfo.AppArmor { | |
| securityOptions = append(securityOptions, "name=apparmor") | |
| } | |
| if sysInfo.Seccomp && supportsSeccomp { | |
| if daemon.seccompProfilePath != config.SeccompProfileDefault { | |
| v.Warnings = append(v.Warnings, "WARNING: daemon is not using the default seccomp profile") | |
| } | |
| securityOptions = append(securityOptions, "name=seccomp,profile="+daemon.seccompProfilePath) | |
| } | |
| if selinux.GetEnabled() { | |
| securityOptions = append(securityOptions, "name=selinux") | |
| } | |
| if uid, gid := daemon.idMapping.RootPair(); uid != 0 || gid != 0 { | |
| securityOptions = append(securityOptions, "name=userns") | |
| } | |
| if Rootless(cfg) { | |
| securityOptions = append(securityOptions, "name=rootless") | |
| } | |
| if cgroupNamespacesEnabled(sysInfo, cfg) { | |
| securityOptions = append(securityOptions, "name=cgroupns") | |
| } | |
| if noNewPrivileges(cfg) { | |
| securityOptions = append(securityOptions, "name=no-new-privileges") | |
| } | |
| v.SecurityOptions = securityOptions | |
| } | |
| func (daemon *Daemon) fillContainerStates(v *system.Info) { | |
| cRunning, cPaused, cStopped := metrics.StateCtr.Get() | |
| v.Containers = cRunning + cPaused + cStopped | |
| v.ContainersPaused = cPaused | |
| v.ContainersRunning = cRunning | |
| v.ContainersStopped = cStopped | |
| } | |
| // fillDebugInfo sets the current debugging state of the daemon, and additional | |
| // debugging information, such as the number of Go-routines, and file descriptors. | |
| // | |
| // Note that this currently always collects the information, but the CLI only | |
| // prints it if the daemon has debug enabled. We should consider to either make | |
| // this information optional (cli to request "with debugging information"), or | |
| // only collect it if the daemon has debug enabled. For the CLI code, see | |
| // https://github.com/docker/cli/blob/v20.10.12/cli/command/system/info.go#L239-L244 | |
| func (daemon *Daemon) fillDebugInfo(ctx context.Context, v *system.Info) { | |
| v.Debug = debug.IsEnabled() | |
| v.NFd = filedescriptors.GetTotalUsedFds(ctx) | |
| v.NGoroutines = runtime.NumGoroutine() | |
| v.NEventsListener = daemon.EventsService.SubscribersCount() | |
| } | |
| // fillContainerdInfo provides information about the containerd configuration | |
| // for debugging purposes. | |
| func (daemon *Daemon) fillContainerdInfo(v *system.Info, cfg *config.Config) { | |
| if cfg.ContainerdAddr == "" { | |
| return | |
| } | |
| v.Containerd = &system.ContainerdInfo{ | |
| Address: cfg.ContainerdAddr, | |
| Namespaces: system.ContainerdNamespaces{ | |
| Containers: cfg.ContainerdNamespace, | |
| Plugins: cfg.ContainerdPluginNamespace, | |
| }, | |
| } | |
| } | |
| func (daemon *Daemon) fillAPIInfo(v *system.Info, cfg *config.Config) { | |
| const warn string = ` | |
| Access to the remote API is equivalent to root access on the host. Refer | |
| to the 'Docker daemon attack surface' section in the documentation for | |
| more information: https://docs.docker.com/go/attack-surface/` | |
| for _, host := range cfg.Hosts { | |
| // cnf.Hosts is normalized during startup, so should always have a scheme/proto | |
| proto, addr, _ := strings.Cut(host, "://") | |
| if proto != "tcp" { | |
| continue | |
| } | |
| const removal = "In future versions this will be a hard failure preventing the daemon from starting! Learn more at: https://docs.docker.com/go/api-security/" | |
| if cfg.TLS == nil || !*cfg.TLS { | |
| v.Warnings = append(v.Warnings, fmt.Sprintf("[DEPRECATION NOTICE]: API is accessible on http://%s without encryption.%s\n%s", addr, warn, removal)) | |
| continue | |
| } | |
| if cfg.TLSVerify == nil || !*cfg.TLSVerify { | |
| v.Warnings = append(v.Warnings, fmt.Sprintf("[DEPRECATION NOTICE]: API is accessible on https://%s without TLS client verification.%s\n%s", addr, warn, removal)) | |
| continue | |
| } | |
| } | |
| } | |
| func (daemon *Daemon) fillDefaultAddressPools(ctx context.Context, v *system.Info, cfg *config.Config) { | |
| _, span := tracing.StartSpan(ctx, "fillDefaultAddressPools") | |
| defer span.End() | |
| for _, pool := range cfg.DefaultAddressPools.Value() { | |
| v.DefaultAddressPools = append(v.DefaultAddressPools, system.NetworkAddressPool{ | |
| Base: pool.Base, | |
| Size: pool.Size, | |
| }) | |
| } | |
| } | |
| func (daemon *Daemon) fillFirewallInfo(v *system.Info) { | |
| if daemon.netController == nil { | |
| return | |
| } | |
| v.FirewallBackend = daemon.netController.FirewallBackend() | |
| } | |
| func hostName(ctx context.Context) string { | |
| ctx, span := tracing.StartSpan(ctx, "hostName") | |
| defer span.End() | |
| hn, err := os.Hostname() | |
| if err != nil { | |
| log.G(ctx).WithError(err).Warn("Could not get hostname") | |
| return "" | |
| } | |
| return hn | |
| } | |
| func kernelVersion(ctx context.Context) string { | |
| ctx, span := tracing.StartSpan(ctx, "kernelVersion") | |
| defer span.End() | |
| var ver string | |
| if kv, err := kernel.GetKernelVersion(); err != nil { | |
| log.G(ctx).WithError(err).Warn("Could not get kernel version") | |
| } else { | |
| ver = kv.String() | |
| } | |
| return ver | |
| } | |
| func memInfo(ctx context.Context) *meminfo.Memory { | |
| ctx, span := tracing.StartSpan(ctx, "memInfo") | |
| defer span.End() | |
| mi, err := meminfo.Read() | |
| if err != nil { | |
| log.G(ctx).WithError(err).Error("Could not read system memory info") | |
| return &meminfo.Memory{} | |
| } | |
| return mi | |
| } | |
| func operatingSystem(ctx context.Context) (operatingSystem string) { | |
| ctx, span := tracing.StartSpan(ctx, "operatingSystem") | |
| defer span.End() | |
| defer metrics.StartTimer(metrics.HostInfoFunctions.WithValues("operating_system"))() | |
| if s, err := operatingsystem.GetOperatingSystem(); err != nil { | |
| log.G(ctx).WithError(err).Warn("Could not get operating system name") | |
| } else { | |
| operatingSystem = s | |
| } | |
| if inContainer, err := operatingsystem.IsContainerized(); err != nil { | |
| log.G(ctx).WithError(err).Error("Could not determine if daemon is containerized") | |
| operatingSystem += " (error determining if containerized)" | |
| } else if inContainer { | |
| operatingSystem += " (containerized)" | |
| } | |
| return operatingSystem | |
| } | |
| func osVersion(ctx context.Context) (version string) { | |
| ctx, span := tracing.StartSpan(ctx, "osVersion") | |
| defer span.End() | |
| defer metrics.StartTimer(metrics.HostInfoFunctions.WithValues("os_version"))() | |
| version, err := operatingsystem.GetOperatingSystemVersion() | |
| if err != nil { | |
| log.G(ctx).WithError(err).Warn("Could not get operating system version") | |
| return "" | |
| } | |
| return version | |
| } | |
| func getEnvAny(names ...string) string { | |
| for _, n := range names { | |
| if val := os.Getenv(n); val != "" { | |
| return val | |
| } | |
| } | |
| return "" | |
| } | |
| func getConfigOrEnv(config string, env ...string) string { | |
| if config != "" { | |
| return config | |
| } | |
| return getEnvAny(env...) | |
| } | |
| // promoteNil converts a nil slice to an empty slice of that type. | |
| // A non-nil slice is returned as is. | |
| func promoteNil[S ~[]E, E any](s S) S { | |
| if s == nil { | |
| return S{} | |
| } | |
| return s | |
| } | |
| // fillDiscoveredDevicesFromDrivers iterates over registered device drivers | |
| // and calls their ListDevices method (if available) to populate system info. | |
| func (daemon *Daemon) fillDiscoveredDevicesFromDrivers(ctx context.Context, v *system.Info, cfg *config.Config) { | |
| ctx, span := tracing.StartSpan(ctx, "daemon.fillDiscoveredDevicesFromDrivers") | |
| defer span.End() | |
| // Make sure v.DiscoveredDevices is initialized to an empty slice instead of nil. | |
| // This ensures that the JSON output is always a valid array, even if no devices are discovered. | |
| v.DiscoveredDevices = []system.DeviceInfo{} | |
| for driverName, driver := range deviceDrivers { | |
| if driver.ListDevices == nil { | |
| log.G(ctx).WithField("driver", driverName).Trace("Device driver does not implement ListDevices method.") | |
| continue | |
| } | |
| ls, err := driver.ListDevices(ctx, cfg) | |
| if err != nil { | |
| log.G(ctx).WithFields(log.Fields{ | |
| "driver": driverName, | |
| "error": err, | |
| }).Warn("Failed to list devices for driver") | |
| v.Warnings = append(v.Warnings, fmt.Sprintf("Failed to list devices from driver '%s': %v", driverName, err)) | |
| continue | |
| } | |
| if len(ls.Warnings) > 0 { | |
| v.Warnings = append(v.Warnings, ls.Warnings...) | |
| } | |
| for _, device := range ls.Devices { | |
| device.Source = driverName | |
| v.DiscoveredDevices = append(v.DiscoveredDevices, device) | |
| } | |
| } | |
| } |