// Package daemon implements the isle daemon, which is a long-running service // managing all isle background tasks and sub-processes for a single network. package daemon import ( "context" "fmt" "isle/bootstrap" "isle/daemon/children" "isle/daemon/daecommon" "isle/daemon/network" "isle/nebula" "isle/toolkit" "sync" "dev.mediocregopher.com/mediocre-go-lib.git/mctx" "dev.mediocregopher.com/mediocre-go-lib.git/mlog" ) // 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 { ChildrenOpts *children.Opts // Defaults to that returned by daecommon.GetEnvVars. EnvVars daecommon.EnvVars } func (o *Opts) withDefaults() *Opts { if o == nil { o = new(Opts) } if o.EnvVars == (daecommon.EnvVars{}) { o.EnvVars = daecommon.GetEnvVars() } return o } var _ RPC = (*Daemon)(nil) // Daemon implements all methods of the Daemon interface, plus others used // to manage this particular implementation. // // Daemon manages all child processes and state required by the isle // service, as well as an HTTP socket over which RPC calls will be served. // // Inner Children instance(s) will be wrapped such that they will be // automatically shutdown and re-created whenever there's changes in the network // which require the configuration to be changed (e.g. a new nebula lighthouse). // During such an inner restart all methods will return ErrRestarting, except // Shutdown which will block until the currently executing restart is finished // and then shutdown cleanly from there. // // While still starting up the Daemon for the first time all methods will return // ErrInitializing, except Shutdown which will block until initialization is // canceled. type Daemon struct { logger *mlog.Logger daemonConfig daecommon.Config envBinDirPath string opts *Opts networksStateDir toolkit.Dir networksRuntimeDir toolkit.Dir l sync.RWMutex network network.Network } // New initializes and returns a Daemon. func New( ctx context.Context, logger *mlog.Logger, daemonConfig daecommon.Config, envBinDirPath string, opts *Opts, ) ( *Daemon, error, ) { d := &Daemon{ logger: logger, daemonConfig: daemonConfig, envBinDirPath: envBinDirPath, opts: opts.withDefaults(), } { h := new(toolkit.MkDirHelper) d.networksStateDir, _ = h.Maybe( d.opts.EnvVars.StateDir.MkChildDir("networks", true), ) d.networksRuntimeDir, _ = h.Maybe( d.opts.EnvVars.RuntimeDir.MkChildDir("networks", true), ) if err := h.Err(); err != nil { return nil, fmt.Errorf("creating networks sub-directories: %w", err) } } loadableNetworks, err := LoadableNetworks(d.networksStateDir) if err != nil { return nil, fmt.Errorf("listing loadable networks: %w", err) } if len(loadableNetworks) > 1 { return nil, fmt.Errorf( "more then one loadable Network found: %+v", loadableNetworks, ) } else if len(loadableNetworks) == 1 { id := loadableNetworks[0].ID ctx = mctx.WithAnnotator(ctx, loadableNetworks[0]) networkStateDir, networkRuntimeDir, err := networkDirs( d.networksStateDir, d.networksRuntimeDir, id, true, ) if err != nil { return nil, fmt.Errorf( "creating sub-directories for network %q: %w", id, err, ) } d.network, err = network.Load( ctx, logger.WithNamespace("network"), id, d.daemonConfig, d.envBinDirPath, networkStateDir, networkRuntimeDir, &network.Opts{ ChildrenOpts: d.opts.ChildrenOpts, }, ) if err != nil { return nil, fmt.Errorf("loading network %q: %w", id, err) } } return d, nil } // CreateNetwork will initialize a new network using the given parameters. // - name: Human-readable name of the network. // - domain: Primary domain name that network services are served under. // - ipNet: An IP subnet, in CIDR form, which will be the overall range of // possible IPs in the network. The first IP in this network range will become // this first host's IP. // - hostName: The name of this first host in the network. // // The daemon on which this is called will become the first host in the network, // and will have full administrative privileges. // // Errors: // - network.ErrInvalidConfig func (d *Daemon) CreateNetwork( ctx context.Context, name, domain string, ipNet nebula.IPNet, hostName nebula.HostName, ) error { creationParams := bootstrap.NewCreationParams(name, domain) ctx = mctx.WithAnnotator(ctx, creationParams) d.l.Lock() defer d.l.Unlock() if d.network != nil { return ErrAlreadyJoined } networkStateDir, networkRuntimeDir, err := networkDirs( d.networksStateDir, d.networksRuntimeDir, creationParams.ID, false, ) if err != nil { return fmt.Errorf( "creating sub-directories for network %q: %w", creationParams.ID, err, ) } d.logger.Info(ctx, "Creating network") n, err := network.Create( ctx, d.logger.WithNamespace("network"), d.daemonConfig, d.envBinDirPath, networkStateDir, networkRuntimeDir, creationParams, ipNet, hostName, &network.Opts{ ChildrenOpts: d.opts.ChildrenOpts, }, ) if err != nil { return fmt.Errorf("creating network: %w", err) } d.logger.Info(ctx, "Network created successfully") d.network = n return nil } // JoinNetwork joins the Daemon to an existing network using the given // Bootstrap. // // Errors: // - ErrAlreadyJoined func (d *Daemon) JoinNetwork( ctx context.Context, newBootstrap network.JoiningBootstrap, ) error { networkID := newBootstrap.Bootstrap.NetworkCreationParams.ID ctx = mctx.WithAnnotator(ctx, newBootstrap.Bootstrap.NetworkCreationParams) d.l.Lock() defer d.l.Unlock() if d.network != nil { return ErrAlreadyJoined } networkStateDir, networkRuntimeDir, err := networkDirs( d.networksStateDir, d.networksRuntimeDir, networkID, false, ) if err != nil { return fmt.Errorf( "creating sub-directories for network %q: %w", networkID, err, ) } d.logger.Info(ctx, "Joining network") n, err := network.Join( ctx, d.logger.WithNamespace("network"), d.daemonConfig, newBootstrap, d.envBinDirPath, networkStateDir, networkRuntimeDir, &network.Opts{ ChildrenOpts: d.opts.ChildrenOpts, }, ) if err != nil { return fmt.Errorf( "joining network %q: %w", networkID, err, ) } d.logger.Info(ctx, "Network joined successfully") d.network = n return nil } func withNetwork[Res any]( ctx context.Context, d *Daemon, fn func(context.Context, network.Network) (Res, error), ) ( Res, error, ) { d.l.RLock() defer d.l.RUnlock() if d.network == nil { var zero Res return zero, ErrNoNetwork } return fn(ctx, d.network) } // GetHost implements the method for the network.RPC interface. func (d *Daemon) GetHosts(ctx context.Context) ([]bootstrap.Host, error) { return withNetwork( ctx, d, func(ctx context.Context, n network.Network) ([]bootstrap.Host, error) { return n.GetHosts(ctx) }, ) } // GetGarageClientParams implements the method for the network.RPC interface. func (d *Daemon) GetGarageClientParams( ctx context.Context, ) ( network.GarageClientParams, error, ) { return withNetwork( ctx, d, func( ctx context.Context, n network.Network, ) ( network.GarageClientParams, error, ) { return n.GetGarageClientParams(ctx) }, ) } // GetNebulaCAPublicCredentials implements the method for the network.RPC // interface. func (d *Daemon) GetNebulaCAPublicCredentials( ctx context.Context, ) ( nebula.CAPublicCredentials, error, ) { return withNetwork( ctx, d, func( ctx context.Context, n network.Network, ) ( nebula.CAPublicCredentials, error, ) { return n.GetNebulaCAPublicCredentials(ctx) }, ) } // RemoveHost implements the method for the network.RPC interface. func (d *Daemon) RemoveHost(ctx context.Context, hostName nebula.HostName) error { _, err := withNetwork( ctx, d, func( ctx context.Context, n network.Network, ) ( struct{}, error, ) { return struct{}{}, n.RemoveHost(ctx, hostName) }, ) return err } // CreateHost implements the method for the network.RPC interface. func (d *Daemon) CreateHost( ctx context.Context, hostName nebula.HostName, opts network.CreateHostOpts, ) ( network.JoiningBootstrap, error, ) { return withNetwork( ctx, d, func( ctx context.Context, n network.Network, ) ( network.JoiningBootstrap, error, ) { return n.CreateHost(ctx, hostName, opts) }, ) } // CreateNebulaCertificate implements the method for the network.RPC interface. func (d *Daemon) CreateNebulaCertificate( ctx context.Context, hostName nebula.HostName, hostPubKey nebula.EncryptingPublicKey, ) ( nebula.Certificate, error, ) { return withNetwork( ctx, d, func( ctx context.Context, n network.Network, ) ( nebula.Certificate, error, ) { return n.CreateNebulaCertificate(ctx, hostName, hostPubKey) }, ) } // Shutdown blocks until all resources held or created by the daemon, // including child processes it has started, have been cleaned up. // // If this returns an error then it's possible that child processes are // still running and are no longer managed. func (d *Daemon) Shutdown() error { d.l.Lock() defer d.l.Unlock() if d.network != nil { return d.network.Shutdown() } return nil //var ( // errCh = make(chan error, len(d.networks)) // errs []error //) //for id := range d.networks { // id := id // n := d.networks[id] // go func() { // if err := n.Shutdown(); err != nil { // errCh <- fmt.Errorf("shutting down network %q: %w", id, err) // } // errCh <- nil // }() //} //for range cap(errCh) { // if err := <-errCh; err != nil { // errs = append(errs, err) // } //} //return errors.Join(errs...) }