//go:generate mockery --name Loader --inpackage --filename loader_mock.go package network import ( "context" "errors" "fmt" "isle/bootstrap" "isle/daemon/children" "isle/daemon/daecommon" "isle/nebula" "isle/toolkit" "os" "path/filepath" "strings" "time" "dev.mediocregopher.com/mediocre-go-lib.git/mlog" ) func networkStateDir( networksStateDir toolkit.Dir, networkID string, mayExist bool, ) ( toolkit.Dir, error, ) { return networksStateDir.MkChildDir(networkID, mayExist) } func networkRuntimeDir( networksRuntimeDir toolkit.Dir, networkID string, mayExist bool, ) ( toolkit.Dir, error, ) { return networksRuntimeDir.MkChildDir(networkID, mayExist) } func cleanupDirs( networkStateDir, networkRuntimeDir toolkit.Dir, ) error { var errs []error if err := os.RemoveAll(networkStateDir.Path); err != nil { errs = append(errs, fmt.Errorf( "removing %q: %w", networkStateDir.Path, err, )) } if err := os.RemoveAll(networkRuntimeDir.Path); err != nil { errs = append(errs, fmt.Errorf( "removing %q: %w", networkRuntimeDir.Path, err, )) } return errors.Join(errs...) } func networkDirs( networksStateDir, networksRuntimeDir toolkit.Dir, networkID string, mayExist bool, ) ( stateDir, runtimeDir toolkit.Dir, err error, ) { h := new(toolkit.MkDirHelper) stateDir, _ = h.Maybe( networkStateDir(networksStateDir, networkID, mayExist), ) runtimeDir, _ = h.Maybe( networkRuntimeDir(networksRuntimeDir, networkID, mayExist), ) err = h.Err() return } // Loader is responsible for joining/creating Networks and making them loadable // later. type Loader interface { // Loadable returns the CreationParams for all Networks which can be Loaded. Loadable(context.Context) ([]bootstrap.CreationParams, error) // StoredConfig returns the NetworkConfig currently stored for the network // with the given ID, or the default NetworkConfig if none is stored. StoredConfig( ctx context.Context, networkID string, ) ( daecommon.NetworkConfig, error, ) // Load initializes and returns a Network instance for a network which was // previously joined or created, and which has the given CreationParams. Load( context.Context, *mlog.Logger, bootstrap.CreationParams, *Opts, ) ( Network, error, ) // Join initializes and returns a Network instance for an existing network // which was not previously joined to on this host. Once Join has been // called for a particular network it will error on subsequent calls for // that same network, Load should be used instead. Join( context.Context, *mlog.Logger, JoiningBootstrap, *Opts, ) ( Network, error, ) // Create initializes and returns a Network for a brand new network which // uses the given creation 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. // // Errors: // - ErrInvalidConfig - If the Opts.Config field is not valid. It must be // non-nil and have at least 3 storage allocations. Create( context.Context, *mlog.Logger, bootstrap.CreationParams, nebula.IPNet, nebula.HostName, *Opts, ) ( Network, error, ) // Leave marks a previously loadable Network as being no longer loadable. Leave(context.Context, bootstrap.CreationParams) error } // LoaderOpts are optional parameters which can be passed in when initializing a // new Loader instance. A nil LoaderOpts is equivalent to a zero value. type LoaderOpts struct { // Defaults to that returned by daecommon.GetEnvVars. EnvVars daecommon.EnvVars constructors constructors // defaults to newConstructors() nowFunc func() time.Time // defaults to time.Now } func (o *LoaderOpts) withDefaults() *LoaderOpts { if o == nil { o = new(LoaderOpts) } if o.EnvVars == (daecommon.EnvVars{}) { o.EnvVars = daecommon.GetEnvVars() } if o.constructors == nil { o.constructors = newConstructors() } if o.nowFunc == nil { o.nowFunc = time.Now } return o } type loader struct { opts *LoaderOpts envBinDirPath string networksStateDir toolkit.Dir networksRuntimeDir toolkit.Dir nebulaDeviceNamer *children.NebulaDeviceNamer } // NewLoader returns a new Loader which will use the given directories to load // and create Network instances. func NewLoader( ctx context.Context, logger *mlog.Logger, envBinDirPath string, opts *LoaderOpts, ) ( Loader, error, ) { opts = opts.withDefaults() if err := migrateToMultiNetworkStateDirectory( ctx, logger.WithNamespace("migration-multi-network-state-dir"), opts.EnvVars.StateDir, ); err != nil { return nil, fmt.Errorf( "migrating to multi-network state directory: %w", err, ) } h := new(toolkit.MkDirHelper) networksStateDir, _ := h.Maybe( opts.EnvVars.StateDir.MkChildDir("networks", true), ) networksRuntimeDir, _ := h.Maybe( opts.EnvVars.RuntimeDir.MkChildDir("networks", true), ) if err := h.Err(); err != nil { return nil, fmt.Errorf("creating networks sub-directories: %w", err) } return &loader{ opts, envBinDirPath, networksStateDir, networksRuntimeDir, children.NewNebulaDeviceNamer(), }, nil } func (l *loader) Loadable( ctx context.Context, ) ( []bootstrap.CreationParams, error, ) { networkStateDirs, err := l.networksStateDir.ChildDirs() if err != nil { return nil, fmt.Errorf( "listing children of %q: %w", l.networksStateDir.Path, err, ) } creationParams := make([]bootstrap.CreationParams, 0, len(networkStateDirs)) for _, networkStateDir := range networkStateDirs { if n := filepath.Base(networkStateDir.Path); strings.HasPrefix(n, ".") { continue } thisCreationParams, err := loadCreationParams(networkStateDir) if err != nil { return nil, fmt.Errorf( "loading creation params from %q: %w", networkStateDir.Path, err, ) } creationParams = append(creationParams, thisCreationParams) } return creationParams, nil } func (l *loader) StoredConfig( _ context.Context, networkID string, ) ( daecommon.NetworkConfig, error, ) { networkStateDir, err := networkStateDir(l.networksStateDir, networkID, true) if err != nil { return daecommon.NetworkConfig{}, fmt.Errorf( "getting network state dir: %w", err, ) } return loadConfig(networkStateDir) } func (l *loader) isJoined(ctx context.Context, networkID string) (bool, error) { allJoinedCreationParams, err := l.Loadable(ctx) if err != nil { return false, fmt.Errorf("getting already joined networks: %w", err) } for _, joinedCreationParams := range allJoinedCreationParams { if joinedCreationParams.ID == networkID { return true, nil } } return false, nil } func (l *loader) Load( ctx context.Context, logger *mlog.Logger, creationParams bootstrap.CreationParams, opts *Opts, ) ( Network, error, ) { networkID := creationParams.ID if isJoined, err := l.isJoined(ctx, networkID); err != nil { return nil, fmt.Errorf("checking if network is already joined: %w", err) } else if !isJoined { return nil, errors.New("network is not yet joined") } networkStateDir, networkRuntimeDir, err := networkDirs( l.networksStateDir, l.networksRuntimeDir, networkID, true, ) if err != nil { return nil, fmt.Errorf( "creating sub-directories for network %q: %w", networkID, err, ) } return l.opts.constructors.load( ctx, logger, l.envBinDirPath, l.nebulaDeviceNamer, networkStateDir, networkRuntimeDir, opts, ) } func (l *loader) Join( ctx context.Context, logger *mlog.Logger, joiningBootstrap JoiningBootstrap, opts *Opts, ) ( Network, error, ) { var ( creationParams = joiningBootstrap.Bootstrap.NetworkCreationParams networkID = creationParams.ID ) networkStateDir, networkRuntimeDir, err := networkDirs( l.networksStateDir, l.networksRuntimeDir, networkID, false, ) if err != nil { return nil, fmt.Errorf( "creating sub-directories for network %q: %w", networkID, err, ) } n, err := l.opts.constructors.join( ctx, logger, l.envBinDirPath, l.nebulaDeviceNamer, joiningBootstrap, networkStateDir, networkRuntimeDir, opts, ) if err != nil { return nil, errors.Join( err, cleanupDirs(networkStateDir, networkRuntimeDir), ) } return n, nil } func (l *loader) Create( ctx context.Context, logger *mlog.Logger, creationParams bootstrap.CreationParams, ipNet nebula.IPNet, hostName nebula.HostName, opts *Opts, ) ( Network, error, ) { networkID := creationParams.ID networkStateDir, networkRuntimeDir, err := networkDirs( l.networksStateDir, l.networksRuntimeDir, networkID, false, ) if err != nil { return nil, fmt.Errorf( "creating sub-directories for network %q: %w", networkID, err, ) } n, err := l.opts.constructors.create( ctx, logger, l.envBinDirPath, l.nebulaDeviceNamer, networkStateDir, networkRuntimeDir, creationParams, ipNet, hostName, opts, ) if err != nil { return nil, errors.Join( err, cleanupDirs(networkStateDir, networkRuntimeDir), ) } return n, nil } func (l *loader) Leave( ctx context.Context, creationParams bootstrap.CreationParams, ) error { networkID := creationParams.ID if isJoined, err := l.isJoined(ctx, networkID); err != nil { return fmt.Errorf("checking if network is already joined: %w", err) } else if !isJoined { return errors.New("network is not yet joined") } networkStateDir, networkRuntimeDir, err := networkDirs( l.networksStateDir, l.networksRuntimeDir, networkID, true, ) if err != nil { return fmt.Errorf( "creating sub-directories for network %q: %w", networkID, err, ) } var ( newNetworkStateDirName = fmt.Sprintf( ".%s.%s.bak", creationParams.ID, l.opts.nowFunc().UTC().Format("20060102-150405"), ) newNetworkStateDirPath = filepath.Join( l.networksStateDir.Path, newNetworkStateDirName, ) ) var errs []error if err := os.Rename( networkStateDir.Path, newNetworkStateDirPath, ); err != nil { errs = append(errs, err) } if err := os.RemoveAll(networkRuntimeDir.Path); err != nil { errs = append(errs, err) } return errors.Join(errs...) }