Implement 'network leave'
This commit is contained in:
parent
53a1dc0cc2
commit
279b70124c
@ -177,6 +177,31 @@ var subCmdNetworkJoin = subCmd{
|
||||
},
|
||||
}
|
||||
|
||||
var subCmdNetworkLeave = subCmd{
|
||||
name: "leave",
|
||||
descr: "Leaves a network which was previously joined or created",
|
||||
do: func(ctx subCmdCtx) error {
|
||||
yesImSure := ctx.flags.Bool("yes-im-sure", false, "Must be given")
|
||||
|
||||
ctx, err := ctx.withParsedFlags(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing flags: %w", err)
|
||||
}
|
||||
|
||||
if !*yesImSure {
|
||||
return errors.New("--yes-im-sure must be given")
|
||||
}
|
||||
|
||||
daemonRPC, err := ctx.newDaemonRPC()
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating daemon RPC client: %w", err)
|
||||
}
|
||||
defer daemonRPC.Close()
|
||||
|
||||
return daemonRPC.LeaveNetwork(ctx)
|
||||
},
|
||||
}
|
||||
|
||||
var subCmdNetworkList = subCmd{
|
||||
name: "list",
|
||||
descr: "Lists all networks which have been joined",
|
||||
@ -290,6 +315,7 @@ var subCmdNetwork = subCmd{
|
||||
return ctx.doSubCmd(
|
||||
subCmdNetworkCreate,
|
||||
subCmdNetworkJoin,
|
||||
subCmdNetworkLeave,
|
||||
subCmdNetworkList,
|
||||
subCmdNetworkGetConfig,
|
||||
)
|
||||
|
@ -106,6 +106,15 @@ func (c *rpcClient) JoinNetwork(ctx context.Context, j1 network.JoiningBootstrap
|
||||
return
|
||||
}
|
||||
|
||||
func (c *rpcClient) LeaveNetwork(ctx context.Context) (err error) {
|
||||
err = c.client.Call(
|
||||
ctx,
|
||||
nil,
|
||||
"LeaveNetwork",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *rpcClient) RemoveHost(ctx context.Context, hostName nebula.HostName) (err error) {
|
||||
err = c.client.Call(
|
||||
ctx,
|
||||
|
@ -257,6 +257,30 @@ func withNetwork[Res any](
|
||||
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(
|
||||
@ -401,7 +425,13 @@ func (d *Daemon) GetConfig(
|
||||
)
|
||||
}
|
||||
|
||||
// SetConfig implements the method for the network.RPC interface.
|
||||
// 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 {
|
||||
|
@ -363,6 +363,35 @@ func TestDaemon_CreateNetwork(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestDaemon_LeaveNetwork(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
var (
|
||||
networkA = network.NewMockNetwork(t)
|
||||
creationParamsA = bootstrap.NewCreationParams("AAA", "a.com")
|
||||
h = newHarness(t, &harnessOpts{
|
||||
expectNetworksLoaded: []expectNetworkLoad{{
|
||||
creationParamsA, nil, networkA,
|
||||
}},
|
||||
expectStoredConfigs: map[string]daecommon.NetworkConfig{
|
||||
creationParamsA.ID: daecommon.NewNetworkConfig(nil),
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
h.networkLoader.
|
||||
On("Leave", toolkit.MockArg[context.Context](), creationParamsA).
|
||||
Return(nil).
|
||||
Once()
|
||||
|
||||
ctx := WithNetwork(h.ctx, "AAA")
|
||||
assert.NoError(t, h.daemon.LeaveNetwork(ctx))
|
||||
|
||||
joinedCreationParams, err := h.daemon.GetNetworks(h.ctx)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, joinedCreationParams)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDaemon_SetConfig(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
var (
|
||||
|
@ -30,6 +30,25 @@ func writeBootstrapToStateDir(
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadBootstrapFromStateDir(
|
||||
stateDirPath string,
|
||||
) (
|
||||
bootstrap.Bootstrap, error,
|
||||
) {
|
||||
var (
|
||||
currBootstrap bootstrap.Bootstrap
|
||||
bootstrapFilePath = bootstrap.StateDirPath(stateDirPath)
|
||||
)
|
||||
|
||||
if err := jsonutil.LoadFile(&currBootstrap, bootstrapFilePath); err != nil {
|
||||
return bootstrap.Bootstrap{}, fmt.Errorf(
|
||||
"loading bootstrap from %q: %w", bootstrapFilePath, err,
|
||||
)
|
||||
}
|
||||
|
||||
return currBootstrap, nil
|
||||
}
|
||||
|
||||
func coalesceNetworkConfigAndBootstrap(
|
||||
networkConfig daecommon.NetworkConfig, hostBootstrap bootstrap.Bootstrap,
|
||||
) (
|
||||
|
@ -12,6 +12,9 @@ import (
|
||||
"isle/nebula"
|
||||
"isle/toolkit"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
|
||||
)
|
||||
@ -132,6 +135,9 @@ type Loader interface {
|
||||
) (
|
||||
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
|
||||
@ -140,7 +146,8 @@ type LoaderOpts struct {
|
||||
// Defaults to that returned by daecommon.GetEnvVars.
|
||||
EnvVars daecommon.EnvVars
|
||||
|
||||
constructors constructors // defaults to newConstructors()
|
||||
constructors constructors // defaults to newConstructors()
|
||||
nowFunc func() time.Time // defaults to time.Now
|
||||
}
|
||||
|
||||
func (o *LoaderOpts) withDefaults() *LoaderOpts {
|
||||
@ -156,6 +163,10 @@ func (o *LoaderOpts) withDefaults() *LoaderOpts {
|
||||
o.constructors = newConstructors()
|
||||
}
|
||||
|
||||
if o.nowFunc == nil {
|
||||
o.nowFunc = time.Now
|
||||
}
|
||||
|
||||
return o
|
||||
}
|
||||
|
||||
@ -226,6 +237,10 @@ func (l *loader) Loadable(
|
||||
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(
|
||||
@ -390,3 +405,50 @@ func (l *loader) Create(
|
||||
|
||||
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...)
|
||||
}
|
||||
|
@ -80,6 +80,24 @@ func (_m *MockLoader) Join(_a0 context.Context, _a1 *mlog.Logger, _a2 JoiningBoo
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Leave provides a mock function with given fields: _a0, _a1
|
||||
func (_m *MockLoader) Leave(_a0 context.Context, _a1 bootstrap.CreationParams) error {
|
||||
ret := _m.Called(_a0, _a1)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Leave")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, bootstrap.CreationParams) error); ok {
|
||||
r0 = rf(_a0, _a1)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Load provides a mock function with given fields: _a0, _a1, _a2, _a3
|
||||
func (_m *MockLoader) Load(_a0 context.Context, _a1 *mlog.Logger, _a2 bootstrap.CreationParams, _a3 *Opts) (Network, error) {
|
||||
ret := _m.Called(_a0, _a1, _a2, _a3)
|
||||
|
@ -3,6 +3,7 @@ package network
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"isle/bootstrap"
|
||||
"isle/daemon/children"
|
||||
@ -12,6 +13,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -40,6 +42,7 @@ func newLoaderHarness(t *testing.T) *loaderHarness {
|
||||
stateDir, _ = rootDir.MkChildDir("state", false)
|
||||
runtimeDir, _ = rootDir.MkChildDir("runtime", false)
|
||||
constructors = newMockConstructors(t)
|
||||
now = time.Date(2024, 12, 17, 16, 15, 14, 0, time.UTC)
|
||||
)
|
||||
|
||||
loader, err := NewLoader(
|
||||
@ -49,6 +52,7 @@ func newLoaderHarness(t *testing.T) *loaderHarness {
|
||||
RuntimeDir: runtimeDir,
|
||||
},
|
||||
constructors: constructors,
|
||||
nowFunc: func() time.Time { return now },
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
@ -159,6 +163,16 @@ func TestLoader_Loadable(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.ElementsMatch(t, allCreationParams, got)
|
||||
})
|
||||
|
||||
t.Run("after Leave", func(t *testing.T) {
|
||||
h := newLoaderHarness(t)
|
||||
h.join(t, allCreationParams[0])
|
||||
h.join(t, allCreationParams[1])
|
||||
assert.NoError(t, h.loader.Leave(h.ctx, allCreationParams[1]))
|
||||
got, err := h.loader.Loadable(h.ctx)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, allCreationParams[:1], got)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoader_Load(t *testing.T) {
|
||||
@ -387,3 +401,35 @@ func TestLoader_Create(t *testing.T) {
|
||||
h.assertDirExists(t, false, networkRuntimeDirPath)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoader_Leave(t *testing.T) {
|
||||
var (
|
||||
creationParams = bootstrap.NewCreationParams("AAA", "a.com")
|
||||
)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
var (
|
||||
h = newLoaderHarness(t)
|
||||
networkStateDirPath = h.networkStateDirPath(creationParams.ID)
|
||||
networkRuntimeDirPath = h.networkRuntimeDirPath(creationParams.ID)
|
||||
)
|
||||
|
||||
h.join(t, creationParams)
|
||||
|
||||
assert.NoError(t, h.loader.Leave(h.ctx, creationParams))
|
||||
h.assertDirExists(t, false, networkStateDirPath)
|
||||
h.assertDirExists(t, false, networkRuntimeDirPath)
|
||||
|
||||
wantStateDirRenamedTo := h.networkStateDirPath(fmt.Sprintf(
|
||||
".%s.20241217-161514.bak",
|
||||
creationParams.ID,
|
||||
))
|
||||
t.Logf("wantStateDirRenamedTo:%q", wantStateDirRenamedTo)
|
||||
h.assertDirExists(t, true, wantStateDirRenamedTo)
|
||||
|
||||
// Make sure the data inside the state directory was actually preserved.
|
||||
bootstrap, err := loadBootstrapFromStateDir(wantStateDirRenamedTo)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, creationParams, bootstrap.NetworkCreationParams)
|
||||
})
|
||||
}
|
||||
|
@ -288,16 +288,12 @@ func (constructorsImpl) load(
|
||||
return nil, fmt.Errorf("instantiating Network: %w", err)
|
||||
}
|
||||
|
||||
var (
|
||||
currBootstrap bootstrap.Bootstrap
|
||||
bootstrapFilePath = bootstrap.StateDirPath(n.stateDir.Path)
|
||||
)
|
||||
currBootstrap, err := loadBootstrapFromStateDir(n.stateDir.Path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("loading bootstrap from state dir: %w", err)
|
||||
}
|
||||
|
||||
if err := jsonutil.LoadFile(&currBootstrap, bootstrapFilePath); err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"loading bootstrap from %q: %w", bootstrapFilePath, err,
|
||||
)
|
||||
} else if err := n.initialize(ctx, currBootstrap, false); err != nil {
|
||||
if err := n.initialize(ctx, currBootstrap, false); err != nil {
|
||||
return nil, fmt.Errorf("initializing with bootstrap: %w", err)
|
||||
}
|
||||
|
||||
|
@ -43,15 +43,10 @@ type RPC interface {
|
||||
|
||||
JoinNetwork(context.Context, network.JoiningBootstrap) error
|
||||
|
||||
LeaveNetwork(context.Context) error
|
||||
|
||||
GetNetworks(context.Context) ([]bootstrap.CreationParams, error)
|
||||
|
||||
// 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.
|
||||
SetConfig(context.Context, daecommon.NetworkConfig) error
|
||||
|
||||
// All network.RPC methods are automatically implemented by Daemon using the
|
||||
|
@ -226,6 +226,24 @@ func (_m *MockRPC) JoinNetwork(_a0 context.Context, _a1 network.JoiningBootstrap
|
||||
return r0
|
||||
}
|
||||
|
||||
// LeaveNetwork provides a mock function with given fields: _a0
|
||||
func (_m *MockRPC) LeaveNetwork(_a0 context.Context) error {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for LeaveNetwork")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context) error); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// RemoveHost provides a mock function with given fields: ctx, hostName
|
||||
func (_m *MockRPC) RemoveHost(ctx context.Context, hostName nebula.HostName) error {
|
||||
ret := _m.Called(ctx, hostName)
|
||||
|
@ -1,8 +0,0 @@
|
||||
---
|
||||
type: task
|
||||
---
|
||||
|
||||
# Implement `network leave` sub-command
|
||||
|
||||
When leaving a network the daemon should remove the now-defunct state-directory,
|
||||
or at least mark it as inactive in some way.
|
Loading…
Reference in New Issue
Block a user