package main import ( "context" "errors" "fmt" "io/fs" "os" "isle/bootstrap" "isle/daemon" "dev.mediocregopher.com/mediocre-go-lib.git/mctx" "dev.mediocregopher.com/mediocre-go-lib.git/mlog" ) var subCmdDaemon = subCmd{ name: "daemon", descr: "Runs the isle daemon (Default if no sub-command given)", do: func(subCmdCtx subCmdCtx) error { flags := subCmdCtx.flagSet(false) daemonConfigPath := flags.StringP( "config-path", "c", "", "Optional path to a daemon.yml file to load configuration from.", ) dumpConfig := flags.Bool( "dump-config", false, "Write the default configuration file to stdout and exit.", ) bootstrapPath := flags.StringP( "bootstrap-path", "b", "", `Path to a bootstrap.json file. This only needs to be provided the first time the daemon is started, after that it is ignored. If the isle binary has a bootstrap built into it then this argument is always optional.`, ) logLevelStr := flags.StringP( "log-level", "l", "info", `Maximum log level which should be output. Values can be "debug", "info", "warn", "error", "fatal". Does not apply to sub-processes`, ) if err := flags.Parse(subCmdCtx.args); err != nil { return fmt.Errorf("parsing flags: %w", err) } ctx := subCmdCtx.ctx if *dumpConfig { return daemon.CopyDefaultConfig(os.Stdout, envAppDirPath) } logLevel := mlog.LevelFromString(*logLevelStr) if logLevel == nil { return fmt.Errorf("couldn't parse log level %q", *logLevelStr) } logger := subCmdCtx.logger.WithMaxLevel(logLevel.Int()) runtimeDirCleanup, err := setupAndLockRuntimeDir(ctx, logger) if err != nil { return fmt.Errorf("setting up runtime directory: %w", err) } defer runtimeDirCleanup() var ( bootstrapStateDirPath = bootstrap.StateDirPath(daemonEnvVars.StateDirPath) bootstrapAppDirPath = bootstrap.AppDirPath(envAppDirPath) hostBootstrapPath string hostBootstrap bootstrap.Bootstrap ) tryLoadBootstrap := func(path string) bool { ctx := mctx.Annotate(ctx, "bootstrapFilePath", path) if err != nil { return false } else if hostBootstrap, err = bootstrap.FromFile(path); errors.Is(err, fs.ErrNotExist) { logger.WarnString(ctx, "bootstrap file not found") err = nil return false } else if err != nil { err = fmt.Errorf("parsing bootstrap.json at %q: %w", path, err) return false } logger.Info(ctx, "bootstrap file found") hostBootstrapPath = path return true } switch { case tryLoadBootstrap(bootstrapStateDirPath): case *bootstrapPath != "" && tryLoadBootstrap(*bootstrapPath): case tryLoadBootstrap(bootstrapAppDirPath): case err != nil: return fmt.Errorf("attempting to load bootstrap.json file: %w", err) default: return errors.New("No bootstrap.json file could be found, and one is not provided with --bootstrap-path") } if hostBootstrapPath != bootstrapStateDirPath { // If the bootstrap file is not being stored in the data dir, copy // it there, so it can be loaded from there next time. if err := writeBootstrapToStateDir(hostBootstrap); err != nil { return fmt.Errorf("writing bootstrap.json to data dir: %w", err) } } daemonConfig, err := daemon.LoadConfig(envAppDirPath, *daemonConfigPath) if err != nil { return fmt.Errorf("loading daemon config: %w", err) } // we update this Host's data using whatever configuration has been // provided by the daemon config. This way the daemon has the most // up-to-date possible bootstrap. This updated bootstrap will later get // updated in garage as a background daemon task, so other hosts will // see it as well. if hostBootstrap, err = coalesceDaemonConfigAndBootstrap(hostBootstrap, daemonConfig); err != nil { return fmt.Errorf("merging daemon config into bootstrap data: %w", err) } daemonInst := daemon.NewDaemonRestarter( logger, daemonConfig, envBinDirPath, hostBootstrap, nil, ) defer func() { logger.Info(ctx, "Stopping child processes") if err := daemonInst.Shutdown(); err != nil { logger.Error(ctx, "Shutting down daemon cleanly failed, there may be orphaned child processes", err) } logger.Info(ctx, "Child processes successfully stopped") }() { logger := logger.WithNamespace("http") httpSrv, err := newHTTPServer( ctx, logger, daemon.NewRPC(daemonInst), ) if err != nil { return fmt.Errorf("starting HTTP server: %w", err) } defer func() { // see comment in daemonInst shutdown logic regarding background // context. logger.Info(ctx, "Shutting down HTTP socket") if err := httpSrv.Shutdown(context.Background()); err != nil { logger.Error(ctx, "Failed to cleanly shutdown http server", err) } }() } <-ctx.Done() return nil }, }