// Package daemon implements the isle daemon, which is a long-running service // managing all isle background tasks and sub-processes for a single cluster. package daemon import ( "context" "fmt" "io" "isle/bootstrap" "os" "time" "code.betamike.com/micropelago/pmux/pmuxlib" "dev.mediocregopher.com/mediocre-go-lib.git/mlog" ) type daemon struct { logger *mlog.Logger config Config hostBootstrap bootstrap.Bootstrap binDirPath string opts Opts pmuxCancelFn context.CancelFunc pmuxStoppedCh chan struct{} } // Daemon presents all functionality required for client frontends to interact // with isle, typically via the unix socket. type Daemon interface { // GetGarageBootstrapHosts loads (and verifies) the .json.signed // file for all hosts stored in garage. GetGarageBootstrapHosts( ctx context.Context, ) ( map[string]bootstrap.Host, error, ) // Shutdown blocks until all resources held or created by the daemon, // including child processes it has started, have been cleaned up, or until // the context is canceled. // // If this returns an error then it's possible that child processes are // still running and are no longer managed. Shutdown(context.Context) error } // Opts are optional parameters which can be passed in when initializing a new // Daemon instance. A nil Opts is equivalent to a zero value. type Opts struct { // SkipHostBootstrapPush, if set, will cause the Daemon to not push the // bootstrap to garage upon a successful initialization. SkipHostBootstrapPush bool // Stdout and Stderr are what the associated outputs from child processes // will be directed to. Stdout, Stderr io.Writer EnvVars EnvVars // Defaults to that returned by GetEnvVars. } func (o *Opts) withDefaults() *Opts { if o == nil { o = new(Opts) } if o.Stdout == nil { o.Stdout = os.Stdout } if o.Stderr == nil { o.Stderr = os.Stderr } if o.EnvVars == (EnvVars{}) { o.EnvVars = GetEnvVars() } return o } // New initialized and returns a Daemon. If initialization fails an error is // returned. func New( ctx context.Context, logger *mlog.Logger, config Config, hostBootstrap bootstrap.Bootstrap, binDirPath string, opts *Opts, ) ( Daemon, error, ) { opts = opts.withDefaults() nebulaPmuxProcConfig, err := nebulaPmuxProcConfig( opts.EnvVars.RuntimeDirPath, binDirPath, hostBootstrap, config, ) if err != nil { return nil, fmt.Errorf("generating nebula config: %w", err) } dnsmasqPmuxProcConfig, err := dnsmasqPmuxProcConfig( opts.EnvVars.RuntimeDirPath, binDirPath, hostBootstrap, config, ) if err != nil { return nil, fmt.Errorf("generating dnsmasq config: %w", err) } garagePmuxProcConfigs, err := garagePmuxProcConfigs( opts.EnvVars.RuntimeDirPath, binDirPath, hostBootstrap, config, ) if err != nil { return nil, fmt.Errorf("generating garage children configs: %w", err) } pmuxConfig := pmuxlib.Config{ Processes: append( []pmuxlib.ProcessConfig{ nebulaPmuxProcConfig, dnsmasqPmuxProcConfig, }, garagePmuxProcConfigs..., ), } pmuxCtx, pmuxCancelFn := context.WithCancel(context.Background()) d := &daemon{ logger: logger, config: config, hostBootstrap: hostBootstrap, binDirPath: binDirPath, opts: *opts, pmuxCancelFn: pmuxCancelFn, pmuxStoppedCh: make(chan struct{}), } go func() { defer close(d.pmuxStoppedCh) pmuxlib.Run(pmuxCtx, d.opts.Stdout, d.opts.Stderr, pmuxConfig) }() if initErr := d.postPmuxInit(ctx); initErr != nil { logger.Warn(ctx, "failed to initialize daemon, shutting down child processes", err) shutdownCtx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) defer cancel() if err := d.Shutdown(shutdownCtx); err != nil { panic(fmt.Sprintf( "failed to shut down child processes after initialization"+ " error, there may be zombie children leftover."+ " Original error: %v", initErr, )) } return nil, initErr } return d, nil } func (d *daemon) postPmuxInit(ctx context.Context) error { d.logger.Info(ctx, "waiting for nebula VPN to come online") if err := waitForNebula(ctx, d.hostBootstrap); err != nil { return fmt.Errorf("waiting for nebula to start: %w", err) } d.logger.Info(ctx, "waiting for garage instances to come online") if err := d.waitForGarage(ctx); err != nil { return fmt.Errorf("waiting for garage to start: %w", err) } if len(d.config.Storage.Allocations) > 0 { err := until(ctx, func(ctx context.Context) error { err := garageApplyLayout(ctx, d.logger, d.hostBootstrap, d.config) if err != nil { d.logger.Error(ctx, "applying garage layout", err) return err } return nil }) if err != nil { return fmt.Errorf("applying garage layout: %w", err) } } if !d.opts.SkipHostBootstrapPush { if err := until(ctx, func(ctx context.Context) error { if err := d.putGarageBoostrapHost(ctx); err != nil { d.logger.Error(ctx, "updating host info in garage", err) return err } return nil }); err != nil { return fmt.Errorf("updating host info in garage: %w", err) } } return nil } func (d *daemon) Shutdown(ctx context.Context) error { d.pmuxCancelFn() select { case <-ctx.Done(): return ctx.Err() case <-d.pmuxStoppedCh: return nil } }