From cedd9f2c99f7f673a4f233911d826e6e164c9fc4 Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Sun, 17 Nov 2024 14:19:46 +0100 Subject: [PATCH] Implement 'storage remove-allocation' --- go/cmd/entrypoint/storage.go | 91 +++++++++++++++++++-- go/cmd/entrypoint/storage_test.go | 131 ++++++++++++++++++++++++++++++ go/daemon/children/nebula.go | 20 ++++- go/daemon/daecommon/config.go | 24 ------ 4 files changed, 232 insertions(+), 34 deletions(-) diff --git a/go/cmd/entrypoint/storage.go b/go/cmd/entrypoint/storage.go index 1c8ba90..6188c08 100644 --- a/go/cmd/entrypoint/storage.go +++ b/go/cmd/entrypoint/storage.go @@ -2,11 +2,37 @@ package main import ( "cmp" + "errors" "fmt" "isle/daemon/daecommon" "slices" + + "golang.org/x/exp/maps" ) +type storageAllocation struct { + Index int `yaml:"index"` + daecommon.ConfigStorageAllocation `yaml:",inline"` +} + +func indexStorageAllocations( + config daecommon.NetworkConfig, +) []storageAllocation { + slices.SortFunc( + config.Storage.Allocations, + func(i, j daecommon.ConfigStorageAllocation) int { + return cmp.Compare(i.RPCPort, j.RPCPort) + }, + ) + + allocs := make([]storageAllocation, len(config.Storage.Allocations)) + for i := range config.Storage.Allocations { + allocs[i] = storageAllocation{i, config.Storage.Allocations[i]} + } + + return allocs +} + var subCmdStorageAllocationList = subCmd{ name: "list-allocation", plural: "s", @@ -22,25 +48,71 @@ var subCmdStorageAllocationList = subCmd{ return nil, fmt.Errorf("getting network config: %w", err) } - type alloc struct { - Index int `yaml:"index"` - daecommon.ConfigStorageAllocation `yaml:",inline"` + return indexStorageAllocations(config), nil + }), +} + +var subCmdStorageAllocationRemove = subCmd{ + name: "remove-allocation", + descr: "Removes an allocation which has been previously added. " + + "Allocations are identified by their index field from the output of " + + "`storage list-allocation(s)`.", + do: func(ctx subCmdCtx) error { + indexes := ctx.flags.IntSlice( + "index", nil, + "Index of the storage allocation which should be removed. Can be "+ + "specified more than once", + ) + + ctx, err := ctx.withParsedFlags() + if err != nil { + return fmt.Errorf("parsing flags: %w", err) } + if len(*indexes) == 0 { + return errors.New("At least one --index must be specified") + } + + config, err := ctx.getDaemonRPC().GetConfig(ctx) + if err != nil { + return fmt.Errorf("getting network config: %w", err) + } + + var ( + allocs = indexStorageAllocations(config) + allocsByIndex = map[int]daecommon.ConfigStorageAllocation{} + ) + + for _, alloc := range allocs { + allocsByIndex[alloc.Index] = alloc.ConfigStorageAllocation + } + + for _, index := range *indexes { + if _, ok := allocsByIndex[index]; !ok { + return fmt.Errorf( + "Index %d not found in configured storage allocations: %w", + index, err, + ) + } + delete(allocsByIndex, index) + } + + // we sort the new allocation set so that tests are deterministic + newAllocs := maps.Values(allocsByIndex) slices.SortFunc( - config.Storage.Allocations, + newAllocs, func(i, j daecommon.ConfigStorageAllocation) int { return cmp.Compare(i.RPCPort, j.RPCPort) }, ) - allocs := make([]alloc, len(config.Storage.Allocations)) - for i := range config.Storage.Allocations { - allocs[i] = alloc{i, config.Storage.Allocations[i]} + config.Storage.Allocations = newAllocs + if err := ctx.getDaemonRPC().SetConfig(ctx, config); err != nil { + return fmt.Errorf("updating the network config: %w", err) } - return allocs, nil - }), + return nil + }, } var subCmdStorage = subCmd{ @@ -49,6 +121,7 @@ var subCmdStorage = subCmd{ do: func(ctx subCmdCtx) error { return ctx.doSubCmd( subCmdStorageAllocationList, + subCmdStorageAllocationRemove, ) }, } diff --git a/go/cmd/entrypoint/storage_test.go b/go/cmd/entrypoint/storage_test.go index e21d83a..e54363d 100644 --- a/go/cmd/entrypoint/storage_test.go +++ b/go/cmd/entrypoint/storage_test.go @@ -2,9 +2,12 @@ package main import ( "context" + "isle/daemon" "isle/daemon/daecommon" "isle/toolkit" "testing" + + "github.com/stretchr/testify/assert" ) func TestStorageAllocationList(t *testing.T) { @@ -83,3 +86,131 @@ func TestStorageAllocationList(t *testing.T) { }) } } + +func TestStorageAllocationRemove(t *testing.T) { + t.Parallel() + + config := func(rpcPorts ...int) daecommon.NetworkConfig { + return daecommon.NewNetworkConfig(func(c *daecommon.NetworkConfig) { + for _, rpcPort := range rpcPorts { + c.Storage.Allocations = append( + c.Storage.Allocations, + daecommon.ConfigStorageAllocation{RPCPort: rpcPort}, + ) + } + }) + } + + tests := []struct { + name string + args []string + setExpectations func(*daemon.MockRPC) + wantErr string + }{ + { + name: "error/no indexes", + args: nil, + wantErr: "At least one --index must be specified", + }, + { + name: "error/unknown index", + args: []string{"--index", "1"}, + setExpectations: func(daemonRPC *daemon.MockRPC) { + daemonRPC. + On("GetConfig", toolkit.MockArg[context.Context]()). + Return(config(1000), nil). + Once() + }, + wantErr: "Index 1 not found", + }, + { + name: "success/remove single", + args: []string{"--index", "0"}, + setExpectations: func(daemonRPC *daemon.MockRPC) { + config := config(1000, 2000) + + daemonRPC. + On("GetConfig", toolkit.MockArg[context.Context]()). + Return(config, nil). + Once() + + config.Storage.Allocations = config.Storage.Allocations[1:] + + daemonRPC. + On( + "SetConfig", + toolkit.MockArg[context.Context](), + config, + ). + Return(nil). + Once() + }, + }, + { + name: "success/remove multiple", + args: []string{"--index", "0", "--index", "2"}, + setExpectations: func(daemonRPC *daemon.MockRPC) { + config := config(1000, 2000, 3000) + + daemonRPC. + On("GetConfig", toolkit.MockArg[context.Context]()). + Return(config, nil). + Once() + + config.Storage.Allocations = config.Storage.Allocations[1:2] + + daemonRPC. + On( + "SetConfig", + toolkit.MockArg[context.Context](), + config, + ). + Return(nil). + Once() + }, + }, + { + name: "success/remove all", + args: []string{"--index", "0", "--index", "1"}, + setExpectations: func(daemonRPC *daemon.MockRPC) { + config := config(1000, 2000) + + daemonRPC. + On("GetConfig", toolkit.MockArg[context.Context]()). + Return(config, nil). + Once() + + config.Storage.Allocations = config.Storage.Allocations[:0] + + daemonRPC. + On( + "SetConfig", + toolkit.MockArg[context.Context](), + config, + ). + Return(nil). + Once() + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + h := newRunHarness(t) + + if test.setExpectations != nil { + test.setExpectations(h.daemonRPC) + } + + args := []string{"storage", "remove-allocation"} + args = append(args, test.args...) + err := h.run(t, args...) + + if test.wantErr == "" { + assert.NoError(t, err) + } else { + assert.Contains(t, err.Error(), test.wantErr) + } + }) + } +} diff --git a/go/daemon/children/nebula.go b/go/daemon/children/nebula.go index 473ecbb..687751d 100644 --- a/go/daemon/children/nebula.go +++ b/go/daemon/children/nebula.go @@ -9,6 +9,7 @@ import ( "isle/toolkit" "net" "path/filepath" + "strconv" "code.betamike.com/micropelago/pmux/pmuxlib" "dev.mediocregopher.com/mediocre-go-lib.git/mctx" @@ -86,6 +87,23 @@ func nebulaConfig( hostBootstrap.PrivateCredentials.EncryptingPrivateKey.Bytes(), ) + firewall := networkConfig.VPN.Firewall + for _, alloc := range networkConfig.Storage.Allocations { + firewall.Inbound = append( + firewall.Inbound, + daecommon.ConfigFirewallRule{ + Port: strconv.Itoa(alloc.S3APIPort), + Proto: "tcp", + Host: "any", + }, + daecommon.ConfigFirewallRule{ + Port: strconv.Itoa(alloc.RPCPort), + Proto: "tcp", + Host: "any", + }, + ) + } + config := map[string]any{ "pki": map[string]string{ "ca": string(caCertPEM), @@ -100,7 +118,7 @@ func nebulaConfig( "tun": map[string]any{ "dev": networkConfig.VPN.Tun.Device, }, - "firewall": networkConfig.VPN.Firewall, + "firewall": firewall, } if publicAddr := networkConfig.VPN.PublicAddr; publicAddr == "" { diff --git a/go/daemon/daecommon/config.go b/go/daemon/daecommon/config.go index ab897e7..f291125 100644 --- a/go/daemon/daecommon/config.go +++ b/go/daemon/daecommon/config.go @@ -8,7 +8,6 @@ import ( "isle/toolkit" "isle/yamlutil" "net" - "strconv" _ "embed" @@ -128,8 +127,6 @@ func (c *NetworkConfig) fillDefaults() { c.VPN.Tun.Device = "isle-tun" } - var firewallGarageInbound []ConfigFirewallRule - for i := range c.Storage.Allocations { if c.Storage.Allocations[i].RPCPort == 0 { c.Storage.Allocations[i].RPCPort = 3900 + (i * 10) @@ -142,28 +139,7 @@ func (c *NetworkConfig) fillDefaults() { if c.Storage.Allocations[i].AdminPort == 0 { c.Storage.Allocations[i].AdminPort = 3902 + (i * 10) } - - alloc := c.Storage.Allocations[i] - - firewallGarageInbound = append( - firewallGarageInbound, - ConfigFirewallRule{ - Port: strconv.Itoa(alloc.S3APIPort), - Proto: "tcp", - Host: "any", - }, - ConfigFirewallRule{ - Port: strconv.Itoa(alloc.RPCPort), - Proto: "tcp", - Host: "any", - }, - ) } - - c.VPN.Firewall.Inbound = append( - c.VPN.Firewall.Inbound, - firewallGarageInbound..., - ) } // UnmarshalYAML implements the yaml.Unmarshaler interface. It will attempt to