| package command | |
| import ( | |
| "context" | |
| "crypto/tls" | |
| "encoding/json" | |
| "fmt" | |
| "net" | |
| "net/http" | |
| "os" | |
| "path/filepath" | |
| "runtime" | |
| "slices" | |
| "strings" | |
| "sync" | |
| "time" | |
| containerddefaults "github.com/containerd/containerd/v2/defaults" | |
| "github.com/containerd/containerd/v2/pkg/tracing" | |
| "github.com/containerd/log" | |
| "github.com/docker/go-connections/tlsconfig" | |
| "github.com/moby/buildkit/session" | |
| "github.com/moby/buildkit/util/tracing/detect" | |
| "github.com/moby/moby/v2/daemon" | |
| buildbackend "github.com/moby/moby/v2/daemon/builder/backend" | |
| "github.com/moby/moby/v2/daemon/builder/dockerfile" | |
| "github.com/moby/moby/v2/daemon/cluster" | |
| "github.com/moby/moby/v2/daemon/command/debug" | |
| "github.com/moby/moby/v2/daemon/command/trap" | |
| "github.com/moby/moby/v2/daemon/config" | |
| buildkit "github.com/moby/moby/v2/daemon/internal/builder-next" | |
| "github.com/moby/moby/v2/daemon/internal/builder-next/exporter" | |
| "github.com/moby/moby/v2/daemon/internal/libcontainerd/supervisor" | |
| "github.com/moby/moby/v2/daemon/internal/otelutil" | |
| "github.com/moby/moby/v2/daemon/internal/rootless" | |
| "github.com/moby/moby/v2/daemon/listeners" | |
| dopts "github.com/moby/moby/v2/daemon/pkg/opts" | |
| "github.com/moby/moby/v2/daemon/pkg/plugin" | |
| apiserver "github.com/moby/moby/v2/daemon/server" | |
| "github.com/moby/moby/v2/daemon/server/middleware" | |
| "github.com/moby/moby/v2/daemon/server/router" | |
| "github.com/moby/moby/v2/daemon/server/router/build" | |
| checkpointrouter "github.com/moby/moby/v2/daemon/server/router/checkpoint" | |
| "github.com/moby/moby/v2/daemon/server/router/container" | |
| debugrouter "github.com/moby/moby/v2/daemon/server/router/debug" | |
| distributionrouter "github.com/moby/moby/v2/daemon/server/router/distribution" | |
| grpcrouter "github.com/moby/moby/v2/daemon/server/router/grpc" //nolint:staticcheck // Deprecated endpoint kept for backward compatibility | |
| "github.com/moby/moby/v2/daemon/server/router/image" | |
| "github.com/moby/moby/v2/daemon/server/router/network" | |
| pluginrouter "github.com/moby/moby/v2/daemon/server/router/plugin" | |
| sessionrouter "github.com/moby/moby/v2/daemon/server/router/session" | |
| swarmrouter "github.com/moby/moby/v2/daemon/server/router/swarm" | |
| systemrouter "github.com/moby/moby/v2/daemon/server/router/system" | |
| "github.com/moby/moby/v2/daemon/server/router/volume" | |
| "github.com/moby/moby/v2/dockerversion" | |
| "github.com/moby/moby/v2/pkg/authorization" | |
| "github.com/moby/moby/v2/pkg/homedir" | |
| "github.com/moby/moby/v2/pkg/pidfile" | |
| "github.com/moby/moby/v2/pkg/plugingetter" | |
| "github.com/moby/sys/userns" | |
| "github.com/pkg/errors" | |
| "github.com/sirupsen/logrus" | |
| "github.com/spf13/pflag" | |
| "go.opentelemetry.io/otel" | |
| "go.opentelemetry.io/otel/bridge/opencensus" | |
| "go.opentelemetry.io/otel/propagation" | |
| "tags.cncf.io/container-device-interface/pkg/cdi" | |
| ) | |
| // daemonCLI represents the daemon CLI. | |
| type daemonCLI struct { | |
| *config.Config | |
| configFile *string | |
| flags *pflag.FlagSet | |
| d *daemon.Daemon | |
| authzMiddleware *authorization.Middleware // authzMiddleware enables to dynamically reload the authorization plugins | |
| stopOnce sync.Once | |
| apiShutdown chan struct{} | |
| apiTLSConfig *tls.Config | |
| } | |
| // newDaemonCLI returns a daemon CLI with the given options. | |
| func newDaemonCLI(opts *daemonOptions) (*daemonCLI, error) { | |
| cfg, err := loadDaemonCliConfig(opts) | |
| if err != nil { | |
| return nil, err | |
| } | |
| tlsConfig, err := newAPIServerTLSConfig(cfg) | |
| if err != nil { | |
| return nil, err | |
| } | |
| if opts.Validate { | |
| // Verify platform-specific requirements. This is checked early so | |
| // that `dockerd --validate` also validates system requirements. | |
| // We only validate when explicitly asked, to allow (un)registering | |
| // the service (`dockerd --register-service`, `dockerd --unregister-service`) | |
| // on Windows to succeed. See https://github.com/moby/moby/issues/51997 | |
| // | |
| // The same check is also performed on start. | |
| if err := daemon.CheckSystem(); err != nil { | |
| return nil, fmt.Errorf("system requirements not met: %w", err) | |
| } | |
| } | |
| return &daemonCLI{ | |
| Config: cfg, | |
| configFile: &opts.configFile, | |
| flags: opts.flags, | |
| apiShutdown: make(chan struct{}), | |
| apiTLSConfig: tlsConfig, | |
| }, nil | |
| } | |
| func (cli *daemonCLI) start(ctx context.Context) (retErr error) { | |
| if err := daemon.CheckSystem(); err != nil { | |
| return fmt.Errorf("system requirements not met: %w", err) | |
| } | |
| configureProxyEnv(ctx, cli.Config.Proxies) | |
| if err := configureDaemonLogs(ctx, cli.Config.DaemonLogConfig); err != nil { | |
| return fmt.Errorf("failed to configure daemon logging: %w", err) | |
| } | |
| log.G(ctx).Info("Starting up") | |
| defer func() { | |
| l := log.G(ctx) | |
| if retErr != nil && !errors.Is(retErr, context.Canceled) { | |
| l = l.WithError(retErr) | |
| } | |
| l.Info("Daemon shutdown complete") | |
| }() | |
| if cli.Config.Debug { | |
| debug.Enable() | |
| } | |
| if rootless.RunningWithRootlessKit() && !cli.Config.IsRootless() { | |
| return errors.New("rootless mode needs to be enabled for running with RootlessKit") | |
| } | |
| // return human-friendly error before creating files | |
| if runtime.GOOS == "linux" && os.Geteuid() != 0 { | |
| return errors.New("dockerd needs to be started with root privileges. To run dockerd in rootless mode as an unprivileged user, see https://docs.docker.com/go/rootless/") | |
| } | |
| if err := setDefaultUmask(); err != nil { | |
| return err | |
| } | |
| // Create the daemon root before we create ANY other files (PID, or migrate keys) | |
| // to ensure the appropriate ACL is set (particularly relevant on Windows) | |
| if err := daemon.CreateDaemonRoot(cli.Config); err != nil { | |
| return err | |
| } | |
| if err := os.MkdirAll(cli.Config.ExecRoot, 0o700); err != nil { | |
| return err | |
| } | |
| potentiallyUnderRuntimeDir := []string{cli.Config.ExecRoot} | |
| if cli.Pidfile != "" { | |
| if err := os.MkdirAll(filepath.Dir(cli.Pidfile), 0o755); err != nil { | |
| return errors.Wrap(err, "failed to create pidfile directory") | |
| } | |
| if err := pidfile.Write(cli.Pidfile, os.Getpid()); err != nil { | |
| return errors.Wrapf(err, "failed to start daemon, ensure docker is not running or delete %s", cli.Pidfile) | |
| } | |
| potentiallyUnderRuntimeDir = append(potentiallyUnderRuntimeDir, cli.Pidfile) | |
| defer func() { | |
| if err := os.Remove(cli.Pidfile); err != nil { | |
| log.G(ctx).Error(err) | |
| } | |
| }() | |
| } | |
| if cli.Config.IsRootless() { | |
| log.G(ctx).Warn("Running in rootless mode. This mode has feature limitations.") | |
| if rootless.RunningWithRootlessKit() { | |
| log.G(ctx).Info("Running with RootlessKit integration") | |
| } | |
| // Set sticky bit if XDG_RUNTIME_DIR is set && the file is actually under XDG_RUNTIME_DIR | |
| if _, err := homedir.StickRuntimeDirContents(potentiallyUnderRuntimeDir); err != nil { | |
| // StickRuntimeDirContents returns nil error if XDG_RUNTIME_DIR is just unset | |
| log.G(ctx).WithError(err).Warn("cannot set sticky bit on files under XDG_RUNTIME_DIR") | |
| } | |
| } | |
| if cli.Config.Experimental { | |
| log.G(ctx).Warn("Running with experimental features enabled") | |
| } | |
| lss, hosts, err := loadListeners(cli.Config, cli.apiTLSConfig) | |
| if err != nil { | |
| return errors.Wrap(err, "failed to load listeners") | |
| } | |
| ctx, cancel := context.WithCancel(ctx) | |
| waitForContainerDShutdown, err := cli.initContainerd(ctx) | |
| if waitForContainerDShutdown != nil { | |
| defer waitForContainerDShutdown(10 * time.Second) | |
| } | |
| if err != nil { | |
| cancel() | |
| return err | |
| } | |
| defer cancel() | |
| httpServer := &http.Server{ | |
| ReadHeaderTimeout: 5 * time.Minute, // "G112: Potential Slowloris Attack (gosec)"; not a real concern for our use, so setting a long timeout. | |
| } | |
| apiShutdownCtx, apiShutdownCancel := context.WithCancel(context.WithoutCancel(ctx)) | |
| apiShutdownDone := make(chan struct{}) | |
| trap.Trap(cli.stop) | |
| go func() { | |
| // Block until cli.stop() has been called. | |
| // It may have already been called, and that's okay. | |
| // Any httpServer.Serve() calls made after | |
| // httpServer.Shutdown() will return immediately, | |
| // which is what we want. | |
| <-cli.apiShutdown | |
| if err := httpServer.Shutdown(apiShutdownCtx); err != nil { | |
| log.G(ctx).WithError(err).Error("Error shutting down http server") | |
| } | |
| close(apiShutdownDone) | |
| }() | |
| defer func() { | |
| select { | |
| case <-cli.apiShutdown: | |
| // cli.stop() has been called and the daemon has completed | |
| // shutting down. Give the HTTP server a little more time to | |
| // finish handling any outstanding requests if needed. | |
| tmr := time.AfterFunc(5*time.Second, apiShutdownCancel) | |
| defer tmr.Stop() | |
| <-apiShutdownDone | |
| default: | |
| // cli.start() has returned without cli.stop() being called, | |
| // e.g. because the daemon failed to start. | |
| // Stop the HTTP server with no grace period. | |
| if err := httpServer.Close(); err != nil { | |
| log.G(ctx).WithError(err).Error("Error closing http server") | |
| } | |
| } | |
| }() | |
| // Notify that the API is active, but before daemon is set up. | |
| if err := preNotifyReady(); err != nil { | |
| return err | |
| } | |
| const otelServiceNameEnv = "OTEL_SERVICE_NAME" | |
| if _, ok := os.LookupEnv(otelServiceNameEnv); !ok { | |
| _ = os.Setenv(otelServiceNameEnv, filepath.Base(os.Args[0])) | |
| } | |
| setOTLPProtoDefault() | |
| otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) | |
| // Initialize the trace recorder for buildkit. | |
| detect.Recorder = detect.NewTraceRecorder() | |
| tp, otelShutdown := otelutil.NewTracerProvider(ctx, true) | |
| otel.SetTracerProvider(tp) | |
| log.G(ctx).Logger.AddHook(tracing.NewLogrusHook()) | |
| // The github.com/microsoft/hcsshim module is instrumented with | |
| // OpenCensus, but we use OpenTelemetry for tracing in the daemon. | |
| // Bridge OpenCensus to OpenTelemetry so OC trace spans are exported to | |
| // the daemon's configured OTEL collector. | |
| opencensus.InstallTraceBridge() | |
| pluginStore := plugin.NewStore() | |
| // Register the CDI driver before the daemon starts, as it might try to restore containers that depend on the CDI driver. | |
| // Note that CDI is not inherently linux-specific, there are some linux-specific assumptions / implementations in the code that | |
| // queries the properties of device on the host as well as performs the injection of device nodes and their access permissions into the OCI spec. | |
| // | |
| // In order to lift this restriction the following would have to be addressed: | |
| // - Support needs to be added to the cdi package for injecting Windows devices: https://tags.cncf.io/container-device-interface/issues/28 | |
| // - The DeviceRequests API must be extended to non-linux platforms. | |
| var cdiCache *cdi.Cache | |
| if cdiEnabled(cli.Config) { | |
| cdiCache = daemon.RegisterCDIDriver(cli.Config.CDISpecDirs...) | |
| } | |
| daemon.RegisterGPUDeviceDrivers(cdiCache) | |
| var apiServer apiserver.Server | |
| authz, err := initMiddlewares(ctx, &apiServer, cli.Config, pluginStore) | |
| if err != nil { | |
| return errors.Wrap(err, "failed to start API server") | |
| } | |
| cli.authzMiddleware = authz | |
| d, err := daemon.NewDaemon(ctx, cli.Config, pluginStore, cli.authzMiddleware) | |
| if err != nil { | |
| return errors.Wrap(err, "failed to start daemon") | |
| } | |
| d.StoreHosts(hosts) | |
| // validate after NewDaemon has restored enabled plugins. Don't change order. | |
| if err := validateAuthzPlugins(cli.Config.AuthorizationPlugins, pluginStore); err != nil { | |
| return errors.Wrap(err, "failed to validate authorization plugin") | |
| } | |
| cli.d = d | |
| if err := startMetricsServer(cli.Config.MetricsAddress); err != nil { | |
| return errors.Wrap(err, "failed to start metrics server") | |
| } | |
| c, err := createAndStartCluster(d, cli.Config) | |
| if err != nil { | |
| return fmt.Errorf("failed to start cluster component: %w", err) | |
| } | |
| // Restart all autostart containers which has a swarm endpoint | |
| // and is not yet running now that we have successfully | |
| // initialized the cluster. | |
| d.RestartSwarmContainers() | |
| b, shutdownBuildKit, err := initBuildkit(ctx, d, cdiCache) | |
| if err != nil { | |
| return fmt.Errorf("error initializing buildkit: %w", err) | |
| } | |
| if runtime.GOOS == "windows" { | |
| if enabled, ok := d.Features()["buildkit"]; ok && enabled { | |
| log.G(ctx).Warn("Buildkit feature is enabled in the daemon.json configuration file. Support for BuildKit on Windows is experimental, and enabling this feature may not work. Use at your own risk!") | |
| } | |
| } | |
| // Enable HTTP/1, HTTP/2 and h2c on the HTTP server. h2c won't be used for *tls.Conn listeners, and HTTP/2 won't be | |
| // used for non-TLS connections. | |
| var p http.Protocols | |
| p.SetHTTP1(true) | |
| p.SetHTTP2(true) | |
| p.SetUnencryptedHTTP2(true) | |
| routers := buildRouters(routerOptions{ | |
| features: d.Features, | |
| daemon: d, | |
| cluster: c, | |
| builder: b, | |
| }) | |
| gs := newGRPCServer(ctx) | |
| b.backend.RegisterGRPC(gs) | |
| httpServer.Protocols = &p | |
| httpServer.Handler = newHTTPHandler(ctx, gs, apiServer.CreateMux(ctx, routers...)) | |
| go d.ProcessClusterNotifications(ctx, c.GetWatchStream()) | |
| cli.setupConfigReloadTrap() | |
| log.G(ctx).Info("Daemon has completed initialization") | |
| // Daemon is fully initialized. Start handling API traffic | |
| // and wait for serve API to complete. | |
| var ( | |
| apiWG sync.WaitGroup | |
| errAPI = make(chan error, 1) | |
| apiStartWG sync.WaitGroup | |
| ) | |
| apiStartWG.Add(len(lss)) | |
| for _, ls := range lss { | |
| apiWG.Go(func() { | |
| log.G(ctx).Infof("API listen on %s", ls.Addr()) | |
| apiStartWG.Done() | |
| if err := httpServer.Serve(ls); err != nil && !errors.Is(err, http.ErrServerClosed) { | |
| log.G(ctx).WithFields(log.Fields{ | |
| "error": err, | |
| "listener": ls.Addr(), | |
| }).Error("ServeAPI error") | |
| select { | |
| case errAPI <- err: | |
| default: | |
| } | |
| } | |
| }) | |
| } | |
| // Wait for all API listeners to start handling requests. | |
| apiStartWG.Wait() | |
| select { | |
| case <-errAPI: | |
| // An API listener failed to start; skip notifying systemd that we're ready. | |
| default: | |
| // All API listeners started handling requests; notify systemd that we're ready. | |
| notifyReady() | |
| } | |
| apiWG.Wait() | |
| close(errAPI) | |
| c.Cleanup() | |
| // notify systemd that we're shutting down | |
| notifyStopping() | |
| shutdownDaemon(ctx, d) | |
| // shutdown / close BuildKit backend | |
| shutdownBuildKit() | |
| // Stop notification processing and any background processes | |
| cancel() | |
| if err, ok := <-errAPI; ok { | |
| return errors.Wrap(err, "shutting down due to ServeAPI error") | |
| } | |
| if err := otelShutdown(context.WithoutCancel(ctx)); err != nil { | |
| log.G(ctx).WithError(err).Error("Failed to shutdown OTEL tracing") | |
| } | |
| return nil | |
| } | |
| // The buildkit "detect" package uses grpc as the default proto, which is in conformance with the old spec. | |
| // For a little while now http/protobuf is the default spec, so this function sets the protocol to http/protobuf when the env var is unset | |
| // so that the detect package will use http/protobuf as a default. | |
| // TODO: This can be removed after buildkit is updated to use http/protobuf as the default. | |
| func setOTLPProtoDefault() { | |
| const ( | |
| tracesEnv = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL" | |
| metricsEnv = "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL" | |
| protoEnv = "OTEL_EXPORTER_OTLP_PROTOCOL" | |
| defaultProto = "http/protobuf" | |
| ) | |
| if os.Getenv(protoEnv) == "" { | |
| if os.Getenv(tracesEnv) == "" { | |
| _ = os.Setenv(tracesEnv, defaultProto) | |
| } | |
| if os.Getenv(metricsEnv) == "" { | |
| _ = os.Setenv(metricsEnv, defaultProto) | |
| } | |
| } | |
| } | |
| func initBuildkit(ctx context.Context, d *daemon.Daemon, cdiCache *cdi.Cache) (_ builderOptions, closeFn func(), _ error) { | |
| log.G(ctx).Info("Initializing buildkit") | |
| closeFn = func() {} | |
| sm, err := session.NewManager() | |
| if err != nil { | |
| return builderOptions{}, closeFn, errors.Wrap(err, "failed to create sessionmanager") | |
| } | |
| manager, err := dockerfile.NewBuildManager(d.BuilderBackend(), d.IdentityMapping()) | |
| if err != nil { | |
| return builderOptions{}, closeFn, err | |
| } | |
| cfg := d.Config() | |
| bk, err := buildkit.New(ctx, buildkit.Opt{ | |
| SessionManager: sm, | |
| Root: filepath.Join(cfg.Root, "buildkit"), | |
| EngineID: d.ID(), | |
| Dist: d.DistributionServices(), | |
| ImageTagger: d.ImageService(), | |
| NetworkController: d.NetworkController(), | |
| DefaultCgroupParent: newCgroupParent(&cfg), | |
| RegistryHosts: d.RegistryHosts, | |
| BuilderConfig: cfg.Builder, | |
| Rootless: daemon.Rootless(&cfg), | |
| IdentityMapping: d.IdentityMapping(), | |
| DNSConfig: cfg.DNSConfig, | |
| ApparmorProfile: daemon.DefaultApparmorProfile(), | |
| UseSnapshotter: d.UsesSnapshotter(), | |
| Snapshotter: d.ImageService().StorageDriver(), | |
| ContainerdAddress: cfg.ContainerdAddr, | |
| ContainerdNamespace: cfg.ContainerdNamespace, | |
| HyperVIsolation: d.DefaultIsolation().IsHyperV(), | |
| Callbacks: exporter.BuildkitCallbacks{ | |
| Exported: d.ImageExportedByBuildkit, | |
| Named: d.ImageNamedByBuildkit, | |
| }, | |
| CDICache: cdiCache, | |
| }) | |
| if err != nil { | |
| return builderOptions{}, closeFn, errors.Wrap(err, "error creating buildkit instance") | |
| } | |
| bb, err := buildbackend.NewBackend(d.ImageService(), manager, bk, d.EventsService) | |
| if err != nil { | |
| return builderOptions{}, closeFn, errors.Wrap(err, "failed to create builder backend") | |
| } | |
| log.G(ctx).Info("Completed buildkit initialization") | |
| closeFn = func() { | |
| if err := bk.Close(); err != nil { | |
| log.G(ctx).WithError(err).Error("Failed to close buildkit") | |
| } | |
| } | |
| return builderOptions{ | |
| backend: bb, | |
| buildkit: bk, | |
| sessionManager: sm, | |
| }, closeFn, nil | |
| } | |
| type routerOptions struct { | |
| features func() map[string]bool | |
| daemon *daemon.Daemon | |
| cluster *cluster.Cluster | |
| builder builderOptions | |
| } | |
| type builderOptions struct { | |
| backend *buildbackend.Backend | |
| buildkit *buildkit.Builder | |
| sessionManager *session.Manager | |
| } | |
| func (cli *daemonCLI) reloadConfig() { | |
| ctx := context.TODO() | |
| log.G(ctx).WithField("config-file", *cli.configFile).Info("Got signal to reload configuration") | |
| reload := func(cfg *config.Config) { | |
| if err := validateAuthzPlugins(cfg.AuthorizationPlugins, cli.d.PluginStore); err != nil { | |
| log.G(ctx).WithError(err).Fatal("Error validating authorization plugin") | |
| return | |
| } | |
| if err := cli.d.Reload(cfg); err != nil { | |
| log.G(ctx).WithError(err).Error("Error reconfiguring the daemon") | |
| return | |
| } | |
| // Apply our own configuration only after the daemon reload has succeeded. We | |
| // don't want to partially apply the config if the daemon is unhappy with it. | |
| cli.authzMiddleware.SetPlugins(cfg.AuthorizationPlugins) | |
| if cfg.IsValueSet("debug") { | |
| debugEnabled := debug.IsEnabled() | |
| switch { | |
| case debugEnabled && !cfg.Debug: // disable debug | |
| debug.Disable() | |
| case cfg.Debug && !debugEnabled: // enable debug | |
| debug.Enable() | |
| } | |
| } | |
| } | |
| // On Linux, we use sd_notify to indicate we're reloading config. We send | |
| // this signal as early as possible to let systemd know we're reloading, | |
| // but must signal "ready" after this completes (even on failure), which | |
| // is done by the reload function defined above. | |
| done := notifyReloading() | |
| defer done() | |
| if err := config.Reload(*cli.configFile, cli.flags, reload); err != nil { | |
| 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") | |
| } | |
| } | |
| func (cli *daemonCLI) stop() { | |
| // Signal that the API server should shut down as soon as possible. | |
| // This construct is used rather than directly shutting down the HTTP | |
| // server to avoid any issues if this method is called before the server | |
| // has been instantiated in cli.start(). If this method is called first, | |
| // the HTTP server will be shut down immediately upon instantiation. | |
| cli.stopOnce.Do(func() { | |
| close(cli.apiShutdown) | |
| }) | |
| } | |
| // shutdownDaemon just wraps daemon.Shutdown() to handle a timeout in case | |
| // d.Shutdown() is waiting too long to kill container or worst it's | |
| // blocked there | |
| func shutdownDaemon(ctx context.Context, d *daemon.Daemon) { | |
| var cancel context.CancelFunc | |
| if timeout := d.ShutdownTimeout(); timeout >= 0 { | |
| ctx, cancel = context.WithTimeout(ctx, time.Duration(timeout)*time.Second) | |
| } else { | |
| ctx, cancel = context.WithCancel(ctx) | |
| } | |
| go func() { | |
| defer cancel() | |
| d.Shutdown(ctx) | |
| }() | |
| <-ctx.Done() | |
| if errors.Is(ctx.Err(), context.DeadlineExceeded) { | |
| log.G(ctx).Error("Force shutdown daemon") | |
| } else { | |
| log.G(ctx).Debug("Clean shutdown succeeded") | |
| } | |
| } | |
| func loadDaemonCliConfig(opts *daemonOptions) (*config.Config, error) { | |
| if !opts.flags.Parsed() { | |
| return nil, errors.New(`cannot load CLI config before flags are parsed`) | |
| } | |
| opts.setDefaultOptions() | |
| conf := opts.daemonConfig | |
| flags := opts.flags | |
| conf.Debug = opts.Debug | |
| conf.Hosts = opts.Hosts | |
| // The DOCKER_MIN_API_VERSION env-var allows overriding the minimum API | |
| // version provided by the daemon within constraints of the minimum and | |
| // maximum (current) supported API versions. | |
| // | |
| // API versions older than [config.defaultMinAPIVersion] are deprecated and | |
| // to be removed in a future release. The "DOCKER_MIN_API_VERSION" env-var | |
| // should only be used for exceptional cases. | |
| if ver := os.Getenv("DOCKER_MIN_API_VERSION"); ver != "" { | |
| if err := config.ValidateMinAPIVersion(ver); err != nil { | |
| return nil, errors.Wrap(err, "invalid DOCKER_MIN_API_VERSION") | |
| } | |
| conf.MinAPIVersion = ver | |
| } | |
| if flags.Changed(FlagTLS) { | |
| conf.TLS = &opts.TLS | |
| } | |
| if flags.Changed(FlagTLSVerify) { | |
| conf.TLSVerify = &opts.TLSVerify | |
| v := true | |
| conf.TLS = &v | |
| } | |
| if opts.TLSOptions != nil { | |
| conf.TLSOptions = config.TLSOptions{ | |
| CAFile: opts.TLSOptions.CAFile, | |
| CertFile: opts.TLSOptions.CertFile, | |
| KeyFile: opts.TLSOptions.KeyFile, | |
| } | |
| } else { | |
| conf.TLSOptions = config.TLSOptions{} | |
| } | |
| if runtime.GOOS == "windows" && opts.configFile == "" { | |
| // On Windows, the location of the config-file is relative to the | |
| // daemon's data-root, which is configurable, so we cannot use a | |
| // fixed default location. Instead, we set the location here. | |
| // | |
| // FIXME(thaJeztah): find a better default on Windows that does not depend on "daemon.json" or the --data-root option. | |
| opts.configFile = filepath.Join(conf.Root, "config", "daemon.json") | |
| } | |
| if opts.configFile != "" { | |
| c, err := config.MergeDaemonConfigurations(conf, flags, opts.configFile) | |
| if err != nil { | |
| if flags.Changed("config-file") || !os.IsNotExist(err) { | |
| return nil, errors.Wrapf(err, "unable to configure the Docker daemon with file %s", opts.configFile) | |
| } | |
| } | |
| // the merged configuration can be nil if the config file didn't exist. | |
| // leave the current configuration as it is if when that happens. | |
| if c != nil { | |
| conf = c | |
| } | |
| } | |
| // TODO(thaJeztah): consider making empty strings an error. Existing behavior allowed for empty strings to be used as default, even if explicitly set (`dockerd -H ""`). | |
| conf.Hosts = slices.DeleteFunc(conf.Hosts, func(h string) bool { | |
| return strings.TrimSpace(h) == "" | |
| }) | |
| if len(conf.Hosts) == 0 { | |
| // Set the default host if no hosts are configured. | |
| // TODO(thaJeztah) can set defaults in config.New() instead? | |
| if conf.TLS != nil && *conf.TLS { | |
| // If no host is configured, but the "--tls" flag is set, we | |
| // default to using a TCP connection instead of a unix-socket | |
| // or named pipe. | |
| // | |
| // See https://github.com/moby/moby/commit/0906195fbbd6f379c163b80f23e4c5a60bcfc5f0 | |
| conf.Hosts = append(conf.Hosts, dopts.DefaultTLSHost) | |
| } else { | |
| // Otherwise use the default unix-socket (Linux) or named pipe (Windows). | |
| h, err := defaultAPISocketPath(honorXDG) | |
| if err != nil { | |
| return nil, err | |
| } | |
| conf.Hosts = append(conf.Hosts, h) | |
| } | |
| } | |
| if err := normalizeHosts(conf); err != nil { | |
| return nil, err | |
| } | |
| if err := config.Validate(conf); err != nil { | |
| return nil, err | |
| } | |
| // Check if duplicate label-keys with different values are found | |
| newLabels, err := config.GetConflictFreeLabels(conf.Labels) | |
| if err != nil { | |
| return nil, err | |
| } | |
| conf.Labels = newLabels | |
| // Regardless of whether the user sets it to true or false, if they | |
| // specify TLSVerify at all then we need to turn on TLS | |
| if conf.IsValueSet(FlagTLSVerify) { | |
| v := true | |
| conf.TLS = &v | |
| } | |
| if conf.TLSVerify == nil && conf.TLS != nil { | |
| conf.TLSVerify = conf.TLS | |
| } | |
| err = validateCPURealtimeOptions(conf) | |
| if err != nil { | |
| return nil, err | |
| } | |
| if conf.CDISpecDirs == nil { | |
| // If the CDISpecDirs is not set at this stage, we set it to the default. | |
| conf.CDISpecDirs = append([]string(nil), cdi.DefaultSpecDirs...) | |
| if rootless.RunningWithRootlessKit() { | |
| // In rootless mode, we add the user-specific CDI spec directory. | |
| xch, err := homedir.GetConfigHome() | |
| if err != nil { | |
| return nil, err | |
| } | |
| xrd, err := homedir.GetRuntimeDir() | |
| if err != nil { | |
| return nil, err | |
| } | |
| conf.CDISpecDirs = append(conf.CDISpecDirs, filepath.Join(xch, "cdi"), filepath.Join(xrd, "cdi")) | |
| } | |
| } | |
| // Filter out CDI spec directories that are not readable, and log appropriately | |
| var cdiSpecDirs []string | |
| for _, dir := range conf.CDISpecDirs { | |
| // Non-existing directories are not filtered out here, as CDI spec directories are allowed to not exist. | |
| if _, err := os.ReadDir(dir); err == nil || errors.Is(err, os.ErrNotExist) { | |
| cdiSpecDirs = append(cdiSpecDirs, dir) | |
| } else { | |
| logLevel := log.ErrorLevel | |
| if userns.RunningInUserNS() && errors.Is(err, os.ErrPermission) { | |
| logLevel = log.DebugLevel | |
| } | |
| log.L.WithField("dir", dir).WithError(err).Log(logLevel, "CDI spec directory cannot be accessed, skipping") | |
| } | |
| } | |
| conf.CDISpecDirs = cdiSpecDirs | |
| if len(conf.CDISpecDirs) == 1 && conf.CDISpecDirs[0] == "" { | |
| // If CDISpecDirs is set to an empty string, we clear it to ensure that CDI is disabled. | |
| conf.CDISpecDirs = nil | |
| } | |
| // Only clear CDISpecDirs if CDI is explicitly disabled | |
| if val, exists := conf.Features["cdi"]; exists && !val { | |
| // If the CDI feature is explicitly disabled, we clear the CDISpecDirs to ensure that CDI is disabled. | |
| conf.CDISpecDirs = nil | |
| } | |
| if err := setPlatformOptions(conf); err != nil { | |
| return nil, err | |
| } | |
| return conf, nil | |
| } | |
| // defaultAPISocketPath returns the default path for the Unix socket (Linux) | |
| // or named pipe (Windows). | |
| // | |
| // When running with rootlessKit, XDG dirs should be preferred, and the | |
| // default is to listen on an unprivileged socket in [XDG_RUNTIME_DIR]. | |
| // | |
| // [XDG_RUNTIME_DIR]: https://specifications.freedesktop.org/basedir/0.8/#variables | |
| func defaultAPISocketPath(honorXDG bool) (string, error) { | |
| if honorXDG { | |
| runtimeDir, err := homedir.GetRuntimeDir() | |
| if err != nil { | |
| return "", err | |
| } | |
| return "unix://" + filepath.Join(runtimeDir, "docker.sock"), nil | |
| } | |
| // default unix-socket (Linux) or named pipe (Windows). | |
| return dopts.DefaultHost, nil | |
| } | |
| // normalizeHosts normalizes the configured config.Hosts and removes duplicates. | |
| // It returns an error if it fails to parse a host. | |
| func normalizeHosts(cfg *config.Config) error { | |
| if len(cfg.Hosts) == 0 { | |
| return errors.New("no hosts specified") | |
| } | |
| hosts := slices.Clone(cfg.Hosts) | |
| for i, h := range hosts { | |
| var err error | |
| hosts[i], err = dopts.ParseDaemonHost(h) | |
| if err != nil { | |
| return err | |
| } | |
| } | |
| slices.Sort(hosts) | |
| cfg.Hosts = slices.Compact(hosts) | |
| return nil | |
| } | |
| func buildRouters(opts routerOptions) []router.Router { | |
| routers := []router.Router{ | |
| // we need to add the checkpoint router before the container router or the DELETE gets masked | |
| checkpointrouter.NewRouter(opts.daemon), | |
| container.NewRouter(opts.daemon), | |
| image.NewRouter( | |
| opts.daemon.ImageService(), | |
| opts.daemon.RegistryService(), | |
| ), | |
| systemrouter.NewRouter(opts.daemon, opts.cluster, opts.builder.buildkit, opts.daemon.Features), | |
| volume.NewRouter(opts.daemon.VolumesService(), opts.cluster), | |
| build.NewRouter(opts.builder.backend, opts.daemon), | |
| sessionrouter.NewRouter(opts.builder.sessionManager), //nolint:staticcheck // Deprecated endpoint kept for backward compatibility | |
| swarmrouter.NewRouter(opts.cluster), | |
| pluginrouter.NewRouter(opts.daemon.PluginManager()), | |
| distributionrouter.NewRouter(opts.daemon.ImageBackend()), | |
| network.NewRouter(opts.daemon, opts.cluster), | |
| debugrouter.NewRouter(), | |
| } | |
| if opts.builder.backend != nil { | |
| routers = append(routers, grpcrouter.NewRouter(opts.builder.backend)) //nolint:staticcheck // Deprecated endpoint kept for backward compatibility | |
| } | |
| if opts.daemon.HasExperimental() { | |
| for _, r := range routers { | |
| for _, route := range r.Routes() { | |
| if experimental, ok := route.(router.ExperimentalRoute); ok { | |
| experimental.Enable() | |
| } | |
| } | |
| } | |
| } | |
| return routers | |
| } | |
| func initMiddlewares(_ context.Context, s *apiserver.Server, cfg *config.Config, pluginStore plugingetter.PluginGetter) (*authorization.Middleware, error) { | |
| exp := middleware.NewExperimentalMiddleware(cfg.Experimental) | |
| s.UseMiddleware(exp) | |
| vm, err := middleware.NewVersionMiddleware(dockerversion.Version, config.MaxAPIVersion, cfg.MinAPIVersion) | |
| if err != nil { | |
| return nil, err | |
| } | |
| s.UseMiddleware(*vm) | |
| authzMiddleware := authorization.NewMiddleware(cfg.AuthorizationPlugins, pluginStore) | |
| s.UseMiddleware(authzMiddleware) | |
| return authzMiddleware, nil | |
| } | |
| func getContainerdDaemonOpts(cfg *config.Config) ([]supervisor.DaemonOpt, error) { | |
| var opts []supervisor.DaemonOpt | |
| if cfg.Debug { | |
| opts = append(opts, supervisor.WithLogLevel("debug")) | |
| } else { | |
| opts = append(opts, supervisor.WithLogLevel(cfg.DaemonLogConfig.LogLevel)) | |
| } | |
| if logFormat := cfg.DaemonLogConfig.LogFormat; logFormat != "" { | |
| opts = append(opts, supervisor.WithLogFormat(logFormat)) | |
| } | |
| if !cfg.CriContainerd { | |
| // CRI support in the managed daemon is currently opt-in. | |
| // | |
| // It's disabled by default, originally because it was listening on | |
| // a TCP connection at 0.0.0.0:10010, which was considered a security | |
| // risk, and could conflict with user's container ports. | |
| // | |
| // Current versions of containerd started now listen on localhost on | |
| // an ephemeral port instead, but could still conflict with container | |
| // ports, and running kubernetes using the static binaries is not a | |
| // common scenario, so we (for now) continue disabling it by default. | |
| // | |
| // Also see https://github.com/containerd/containerd/issues/2483#issuecomment-407530608 | |
| opts = append(opts, supervisor.WithCRIDisabled()) | |
| } | |
| if runtime.GOOS == "windows" { | |
| opts = append(opts, supervisor.WithDetectLocalBinary()) | |
| } | |
| return opts, nil | |
| } | |
| func newAPIServerTLSConfig(cfg *config.Config) (*tls.Config, error) { | |
| var tlsConfig *tls.Config | |
| if cfg.TLS != nil && *cfg.TLS { | |
| var ( | |
| clientAuth tls.ClientAuthType | |
| err error | |
| ) | |
| if cfg.TLSVerify == nil || *cfg.TLSVerify { | |
| // server requires and verifies client's certificate | |
| clientAuth = tls.RequireAndVerifyClientCert | |
| } | |
| tlsConfig, err = tlsconfig.Server(tlsconfig.Options{ | |
| CAFile: cfg.TLSOptions.CAFile, | |
| CertFile: cfg.TLSOptions.CertFile, | |
| KeyFile: cfg.TLSOptions.KeyFile, | |
| ExclusiveRootPools: true, | |
| ClientAuth: clientAuth, | |
| }) | |
| if err != nil { | |
| return nil, errors.Wrap(err, "invalid TLS configuration") | |
| } | |
| tlsConfig.NextProtos = []string{"h2", "http/1.1"} | |
| } | |
| return tlsConfig, nil | |
| } | |
| // checkTLSAuthOK checks basically for an explicitly disabled TLS/TLSVerify | |
| // Going forward we do not want to support a scenario where dockerd listens | |
| // on TCP without either TLS client auth (or an explicit opt-in to disable it) | |
| func checkTLSAuthOK(cfg *config.Config) bool { | |
| if cfg.TLS == nil { | |
| // Either TLS is enabled by default, in which case TLS verification should be enabled by default, or explicitly disabled | |
| // Or TLS is disabled by default... in any of these cases, we can just take the default value as to how to proceed | |
| return DefaultTLSValue | |
| } | |
| if !*cfg.TLS { | |
| // TLS is explicitly disabled, which is supported | |
| return true | |
| } | |
| if cfg.TLSVerify == nil { | |
| // this actually shouldn't happen since we set TLSVerify on the config object anyway | |
| // But in case it does get here, be cautious and assume this is not supported. | |
| return false | |
| } | |
| // Either TLSVerify is explicitly enabled or disabled, both cases are supported | |
| return true | |
| } | |
| func loadListeners(cfg *config.Config, tlsConfig *tls.Config) ([]net.Listener, []string, error) { | |
| ctx := context.TODO() | |
| if len(cfg.Hosts) == 0 { | |
| return nil, nil, errors.New("no hosts configured") | |
| } | |
| var ( | |
| hosts []string | |
| lss []net.Listener | |
| ) | |
| for i := 0; i < len(cfg.Hosts); i++ { | |
| protoAddr := cfg.Hosts[i] | |
| proto, addr, ok := strings.Cut(protoAddr, "://") | |
| if !ok { | |
| return nil, nil, fmt.Errorf("bad format %s, expected PROTO://ADDR", protoAddr) | |
| } | |
| // It's a bad idea to bind to TCP without tlsverify. | |
| authEnabled := tlsConfig != nil && tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert | |
| if proto == "tcp" && !authEnabled { | |
| log.G(ctx).WithField("host", protoAddr).Warn("Binding to IP address without --tlsverify is insecure and gives root access on this machine to everyone who has access to your network.") | |
| log.G(ctx).WithField("host", protoAddr).Warn("Binding to an IP address, even on localhost, can also give access to scripts run in a browser. Be safe out there!") | |
| log.G(ctx).WithField("host", protoAddr).Warn("[DEPRECATION NOTICE] In future versions this will be a hard failure preventing the daemon from starting! Learn more at: https://docs.docker.com/go/api-security/") | |
| time.Sleep(time.Second) | |
| // If TLSVerify is explicitly set to false we'll take that as "Please let me shoot myself in the foot" | |
| // We do not want to continue to support a default mode where tls verification is disabled, so we do some extra warnings here and eventually remove support | |
| if !checkTLSAuthOK(cfg) { | |
| ipAddr, _, err := net.SplitHostPort(addr) | |
| if err != nil { | |
| return nil, nil, errors.Wrap(err, "error parsing tcp address") | |
| } | |
| // shortcut all this extra stuff for literal "localhost" | |
| // -H supports specifying hostnames, since we want to bypass this on loopback interfaces we'll look it up here. | |
| if ipAddr != "localhost" { | |
| ip := net.ParseIP(ipAddr) | |
| if ip == nil { | |
| ipA, err := net.ResolveIPAddr("ip", ipAddr) | |
| if err != nil { | |
| log.G(ctx).WithError(err).WithField("host", ipAddr).Error("Error looking up specified host address") | |
| } | |
| if ipA != nil { | |
| ip = ipA.IP | |
| } | |
| } | |
| if ip == nil || !ip.IsLoopback() { | |
| log.G(ctx).WithField("host", protoAddr).Warn("Binding to an IP address without --tlsverify is deprecated. Startup is intentionally being slowed down to show this message") | |
| log.G(ctx).WithField("host", protoAddr).Warn("Please consider generating tls certificates with client validation to prevent exposing unauthenticated root access to your network") | |
| log.G(ctx).WithField("host", protoAddr).Warnf("You can override this by explicitly specifying '--%s=false' or '--%s=false'", FlagTLS, FlagTLSVerify) | |
| log.G(ctx).WithField("host", protoAddr).Warnf("Support for listening on TCP without authentication or explicit intent to run without authentication will be removed in the next release") | |
| time.Sleep(15 * time.Second) | |
| } | |
| } | |
| } | |
| } | |
| // If we're binding to a TCP port, make sure that a container doesn't try to use it. | |
| if proto == "tcp" { | |
| if err := allocateDaemonPort(addr); err != nil { | |
| return nil, nil, err | |
| } | |
| } | |
| ls, err := listeners.Init(proto, addr, cfg.SocketGroup, tlsConfig) | |
| if err != nil { | |
| return nil, nil, err | |
| } | |
| log.G(ctx).Debugf("Listener created for HTTP on %s (%s)", proto, addr) | |
| hosts = append(hosts, addr) | |
| lss = append(lss, ls...) | |
| } | |
| return lss, hosts, nil | |
| } | |
| func createAndStartCluster(d *daemon.Daemon, cfg *config.Config) (*cluster.Cluster, error) { | |
| name, _ := os.Hostname() | |
| c, err := cluster.New(cluster.Config{ | |
| Root: cfg.Root, | |
| Name: name, | |
| Backend: d, | |
| VolumeBackend: d.VolumesService(), | |
| ImageBackend: d.ImageBackend(), | |
| PluginBackend: d.PluginManager(), | |
| NetworkSubnetsProvider: d, | |
| DefaultAdvertiseAddr: cfg.SwarmDefaultAdvertiseAddr, | |
| RaftHeartbeatTick: cfg.SwarmRaftHeartbeatTick, | |
| RaftElectionTick: cfg.SwarmRaftElectionTick, | |
| RuntimeRoot: getSwarmRunRoot(cfg), | |
| }) | |
| if err != nil { | |
| return nil, err | |
| } | |
| d.SetCluster(c) | |
| err = c.Start() | |
| return c, err | |
| } | |
| // validates that the plugins requested with the --authorization-plugin flag are valid AuthzDriver | |
| // plugins present on the host and available to the daemon | |
| func validateAuthzPlugins(requestedPlugins []string, pg plugingetter.PluginGetter) error { | |
| for _, reqPlugin := range requestedPlugins { | |
| if _, err := pg.Get(reqPlugin, authorization.AuthZApiImplements, plugingetter.Lookup); err != nil { | |
| return err | |
| } | |
| } | |
| return nil | |
| } | |
| func systemContainerdRunning(honorXDG bool) (string, bool, error) { | |
| addr := containerddefaults.DefaultAddress | |
| if honorXDG { | |
| runtimeDir, err := homedir.GetRuntimeDir() | |
| if err != nil { | |
| return "", false, err | |
| } | |
| addr = filepath.Join(runtimeDir, "containerd", "containerd.sock") | |
| } | |
| _, err := os.Lstat(addr) | |
| return addr, err == nil, nil | |
| } | |
| // configureDaemonLogs sets the logging level and formatting. It expects | |
| // the passed configuration to already be validated, and ignores invalid options. | |
| func configureDaemonLogs(ctx context.Context, conf config.DaemonLogConfig) error { | |
| switch conf.LogFormat { | |
| case log.JSONFormat: | |
| if err := log.SetFormat(log.JSONFormat); err != nil { | |
| return err | |
| } | |
| case log.TextFormat, "": | |
| if err := log.SetFormat(log.TextFormat); err != nil { | |
| return err | |
| } | |
| if conf.RawLogs { | |
| // FIXME(thaJeztah): this needs a better solution: containerd doesn't allow disabling colors, and this code is depending on internal knowledge of "log.SetFormat" | |
| if l, ok := log.L.Logger.Formatter.(*logrus.TextFormatter); ok { | |
| l.DisableColors = true | |
| } | |
| } | |
| default: | |
| return fmt.Errorf("unknown log format: %s", conf.LogFormat) | |
| } | |
| logLevel := conf.LogLevel | |
| if logLevel == "" { | |
| logLevel = "info" | |
| } | |
| if err := log.SetLevel(logLevel); err != nil { | |
| log.G(ctx).WithError(err).Warn("configure log level") | |
| } | |
| return nil | |
| } | |
| func configureProxyEnv(ctx context.Context, cfg config.Proxies) { | |
| if p := cfg.HTTPProxy; p != "" { | |
| overrideProxyEnv(ctx, "HTTP_PROXY", p) | |
| overrideProxyEnv(ctx, "http_proxy", p) | |
| } | |
| if p := cfg.HTTPSProxy; p != "" { | |
| overrideProxyEnv(ctx, "HTTPS_PROXY", p) | |
| overrideProxyEnv(ctx, "https_proxy", p) | |
| } | |
| if p := cfg.NoProxy; p != "" { | |
| overrideProxyEnv(ctx, "NO_PROXY", p) | |
| overrideProxyEnv(ctx, "no_proxy", p) | |
| } | |
| } | |
| func overrideProxyEnv(ctx context.Context, name, val string) { | |
| if oldVal := os.Getenv(name); oldVal != "" && oldVal != val { | |
| log.G(ctx).WithFields(log.Fields{ | |
| "name": name, | |
| "old-value": config.MaskCredentials(oldVal), | |
| "new-value": config.MaskCredentials(val), | |
| }).Warn("overriding existing proxy variable with value from configuration") | |
| } | |
| _ = os.Setenv(name, val) | |
| } | |
| func (cli *daemonCLI) initializeContainerd(ctx context.Context) (func(time.Duration) error, error) { | |
| systemContainerdAddr, ok, err := systemContainerdRunning(honorXDG) | |
| if err != nil { | |
| return nil, errors.Wrap(err, "could not determine whether the system containerd is running") | |
| } | |
| if ok { | |
| // detected a system containerd at the given address. | |
| cli.Config.ContainerdAddr = systemContainerdAddr | |
| return nil, nil | |
| } | |
| log.G(ctx).Info("containerd not running, starting managed containerd") | |
| opts, err := getContainerdDaemonOpts(cli.Config) | |
| if err != nil { | |
| return nil, errors.Wrap(err, "failed to generate containerd options") | |
| } | |
| r, err := supervisor.Start(ctx, filepath.Join(cli.Config.Root, "containerd"), filepath.Join(cli.Config.ExecRoot, "containerd"), opts...) | |
| if err != nil { | |
| return nil, errors.Wrap(err, "failed to start containerd") | |
| } | |
| cli.Config.ContainerdAddr = r.Address() | |
| // Try to wait for containerd to shutdown | |
| return r.WaitTimeout, nil | |
| } | |
| // cdiEnabled returns true if CDI feature wasn't explicitly disabled via | |
| // features. | |
| func cdiEnabled(conf *config.Config) bool { | |
| if runtime.GOOS != "linux" { | |
| return false | |
| } | |
| val, ok := conf.Features["cdi"] | |
| if !ok { | |
| return true | |
| } | |
| return val | |
| } |