// 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" "errors" "fmt" "isle/bootstrap" "isle/daemon/daecommon" "isle/daemon/network" "isle/nebula" "sort" "sync" "dev.mediocregopher.com/mediocre-go-lib.git/mlog" ) var _ RPC = (*Daemon)(nil) type joinedNetwork struct { id string network.Network creationParams bootstrap.CreationParams config *daecommon.NetworkConfig } // 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 networkLoader network.Loader daemonConfig daecommon.Config l sync.RWMutex networks map[string]joinedNetwork } // New initializes and returns a Daemon. func New( ctx context.Context, logger *mlog.Logger, networkLoader network.Loader, daemonConfig daecommon.Config, ) ( *Daemon, error, ) { d := &Daemon{ logger: logger, networkLoader: networkLoader, daemonConfig: daemonConfig, networks: map[string]joinedNetwork{}, } loadableNetworks, err := networkLoader.Loadable(ctx) if err != nil { return nil, fmt.Errorf("listing loadable networks: %w", err) } if getDeprecatedNetworkConfig(daemonConfig.Networks) != nil && len(loadableNetworks) > 1 { return nil, errors.New("Deprecated config format cannot be used when there are more than one loadable networks") } if err := validateConfig( ctx, networkLoader, daemonConfig, loadableNetworks, ); err != nil { return nil, fmt.Errorf("validating daemon config: %w", err) } for _, creationParams := range loadableNetworks { var ( id = creationParams.ID networkConfig = pickNetworkConfig( daemonConfig.Networks, creationParams, ) ) n, err := networkLoader.Load( ctx, networkLogger(logger, creationParams), creationParams, &network.Opts{ Config: networkConfig, }, ) if err != nil { return nil, fmt.Errorf("loading network %q: %w", id, err) } d.networks[id] = joinedNetwork{id, n, creationParams, networkConfig} } 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] // - [ErrAlreadyJoined] // - [ErrManagedNetworkConfig] - if `opts.NetworkConfig` is given, but a // NetworkConfig is also provided in the Daemon's Config. func (d *Daemon) CreateNetwork( ctx context.Context, creationParams bootstrap.CreationParams, ipNet nebula.IPNet, hostName nebula.HostName, opts *CreateNetworkOpts, ) error { opts = opts.withDefaults() d.l.Lock() defer d.l.Unlock() if getDeprecatedNetworkConfig(d.daemonConfig.Networks) != nil && len(d.networks) > 0 { return network.ErrInvalidConfig.WithData( "Cannot use deprecated configuration file format while joined to more than one network", ) } var ( networkConfig = pickNetworkConfig( d.daemonConfig.Networks, creationParams, ) networkLogger = networkLogger(d.logger, creationParams) ) if opts.Config != nil { if networkConfig != nil { return ErrManagedNetworkConfig } networkConfig = opts.Config } if alreadyJoined(d.networks, creationParams) { return ErrAlreadyJoined } networkLogger.Info(ctx, "Creating network") n, err := d.networkLoader.Create( ctx, networkLogger, creationParams, ipNet, hostName, &network.Opts{ Config: networkConfig, }, ) if err != nil { return fmt.Errorf("creating network: %w", err) } networkLogger.Info(ctx, "Network created successfully") d.networks[creationParams.ID] = joinedNetwork{ creationParams.ID, n, creationParams, networkConfig, } 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 { d.l.Lock() defer d.l.Unlock() if getDeprecatedNetworkConfig(d.daemonConfig.Networks) != nil && len(d.networks) > 0 { return network.ErrInvalidConfig.WithData( "Cannot use deprecated configuration file format while joined to more than one network", ) } var ( creationParams = newBootstrap.Bootstrap.NetworkCreationParams networkID = creationParams.ID networkConfig = pickNetworkConfig( d.daemonConfig.Networks, creationParams, ) networkLogger = networkLogger( d.logger, newBootstrap.Bootstrap.NetworkCreationParams, ) ) if alreadyJoined(d.networks, creationParams) { return ErrAlreadyJoined } networkLogger.Info(ctx, "Joining network") n, err := d.networkLoader.Join( ctx, networkLogger, newBootstrap, &network.Opts{ Config: networkConfig, }, ) if err != nil { return fmt.Errorf( "joining network %q: %w", networkID, err, ) } networkLogger.Info(ctx, "Network joined successfully") d.networks[networkID] = joinedNetwork{ networkID, n, creationParams, networkConfig, } return nil } func withNetwork[Res any]( ctx context.Context, d *Daemon, fn func(context.Context, joinedNetwork) (Res, error), ) ( Res, error, ) { d.l.RLock() defer d.l.RUnlock() network, err := pickNetwork(ctx, d.networkLoader, d.networks) if err != nil { var zero Res return zero, err } return fn(ctx, network) } // LeaveNetwork picks the network out of the Context which was embedded by // WithNetwork, and leaves it. The Network will no longer be considered joined // and will not be active after this returns. // // Errors: // - ErrNoNetwork // - ErrNoMatchingNetworks // - ErrMultipleMatchingNetworks func (d *Daemon) LeaveNetwork(ctx context.Context) error { d.l.Lock() defer d.l.Unlock() network, err := pickNetwork(ctx, d.networkLoader, d.networks) if err != nil { return err } shutdownErr := network.Shutdown() loaderLeaveErr := d.networkLoader.Leave(ctx, network.creationParams) delete(d.networks, network.creationParams.ID) return errors.Join(shutdownErr, loaderLeaveErr) } // GetNetworks returns all networks which have been joined by the Daemon, // ordered by their name. func (d *Daemon) GetNetworks( ctx context.Context, ) ( []bootstrap.CreationParams, error, ) { d.l.RLock() defer d.l.RUnlock() res := make([]bootstrap.CreationParams, 0, len(d.networks)) for id, network := range d.networks { creationParams, err := network.GetNetworkCreationParams(ctx) if err != nil { return nil, fmt.Errorf( "getting network creation params of network %q: %w", id, err, ) } res = append(res, creationParams) } sort.Slice(res, func(i, j int) bool { return res[i].Name < res[j].Name }) return res, nil } // GetBootstrap implements the method for the network.RPC interface. func (d *Daemon) GetBootstrap( ctx context.Context, ) ( bootstrap.Bootstrap, error, ) { return withNetwork( ctx, d, func( ctx context.Context, n joinedNetwork, ) ( bootstrap.Bootstrap, error, ) { return n.GetBootstrap(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 joinedNetwork, ) ( network.GarageClientParams, error, ) { return n.GetGarageClientParams(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 joinedNetwork, ) ( 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 joinedNetwork, ) ( 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 joinedNetwork, ) ( nebula.Certificate, error, ) { return n.CreateNebulaCertificate(ctx, hostName, hostPubKey) }, ) } // GetConfig implements the method for the network.RPC interface. func (d *Daemon) GetConfig( ctx context.Context, ) ( daecommon.NetworkConfig, error, ) { return withNetwork( ctx, d, func( ctx context.Context, n joinedNetwork, ) ( daecommon.NetworkConfig, error, ) { return n.GetConfig(ctx) }, ) } // SetConfig extends the [network.RPC] method of the same name such that // [ErrManagedNetworkConfig] is returned if the picked network is // configured as part of the [daecommon.Config] which the Daemon was // initialized with. // // See the `network.RPC` documentation in this interface for more usage // details. func (d *Daemon) SetConfig( ctx context.Context, networkConfig daecommon.NetworkConfig, ) error { d.l.RLock() defer d.l.RUnlock() pickedNetwork, err := pickNetwork(ctx, d.networkLoader, d.networks) if err != nil { return err } if pickedNetwork.config != nil { return ErrManagedNetworkConfig } // Reconstruct the daemon config using the actual in-use network configs, // along with this new one, and do a validation before calling SetConfig. daemonConfig := d.daemonConfig daemonConfig.Networks = map[string]daecommon.NetworkConfig{ pickedNetwork.id: networkConfig, } for id, joinedNetwork := range d.networks { if pickedNetwork.id == id { continue } joinedNetworkConfig, err := joinedNetwork.GetConfig(ctx) if err != nil { return fmt.Errorf("getting network config of %q: %w", id, err) } daemonConfig.Networks[id] = joinedNetworkConfig } if err := daemonConfig.Validate(); err != nil { return network.ErrInvalidConfig.WithData(err.Error()) } return pickedNetwork.SetConfig(ctx, networkConfig) } // 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() var ( errCh = make(chan error, len(d.networks)) errs []error ) for id := range d.networks { var ( 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) { errs = append(errs, <-errCh) } return errors.Join(errs...) }