Allow creating a network without configuring it in daemon.yml

This commit is contained in:
Brian Picciano 2024-12-17 11:33:19 +01:00
parent 73af69fa04
commit 3111d2ca74
19 changed files with 488 additions and 64 deletions

View File

@ -30,7 +30,7 @@ The requirements for this host are:
* At least 3 directories should be chosen, each of which will be committing at * At least 3 directories should be chosen, each of which will be committing at
least 1GB. Ideally these directories should be on different physical disks, least 1GB. Ideally these directories should be on different physical disks,
but if that's not possible it's ok. See the Next Steps section. but if that's not possible it's ok.
* None of the resources being used for this network (the UDP port or storage * None of the resources being used for this network (the UDP port or storage
locations) should be being used by other networks. locations) should be being used by other networks.

View File

@ -25,8 +25,12 @@ create the signature.
## Releasing ## Releasing
Releases are uploaded to the repository's Releases page, and release naming Release artifactes are hosted at `micropelago.net` under
follows the conventional semantic versioning system. Each release should be `/isle/releases/<release name>`. An `index.gmi` page should be created in that
accompanied by a set of changes which have occurred since the last release, directory which includes links to each artifact, as well as a changelog
described both in the `CHANGELOG.md` file and in the description on the Release detailing all new features and fixes included since the previous release.
itself.
A link to the new release should be included at `/isle/releases/index.gmi`.
The release shoulld be tagged in the git repo using its release name as well,
with the tag notes linking to the `micropelago.net` page.

View File

@ -11,6 +11,11 @@ go test ./... # Test everything
## Integration Tests ## Integration Tests
NOTE: before running integration tests you will want to make sure you can [build
Isle][building] in the first place.
[building]: ./building.md
Integration tests are those which require processes or state external to the Integration tests are those which require processes or state external to the
test itself. Integration tests are marked using the test itself. Integration tests are marked using the
`toolkit.MarkIntegrationTest` function, which will cause them to be skipped `toolkit.MarkIntegrationTest` function, which will cause them to be skipped

View File

@ -6,7 +6,9 @@ import (
"fmt" "fmt"
"isle/nebula" "isle/nebula"
"isle/toolkit" "isle/toolkit"
"net"
"net/netip" "net/netip"
"strconv"
"dev.mediocregopher.com/mediocre-go-lib.git/mlog" "dev.mediocregopher.com/mediocre-go-lib.git/mlog"
) )
@ -34,14 +36,41 @@ func (f *textUnmarshalerFlag[T, P]) String() string {
func (f *textUnmarshalerFlag[T, P]) Type() string { return "string" } func (f *textUnmarshalerFlag[T, P]) Type() string { return "string" }
////////////////////////////////////////////////////////////////////////////////
type ( type (
hostNameFlag = textUnmarshalerFlag[nebula.HostName, *nebula.HostName] hostNameFlag = textUnmarshalerFlag[nebula.HostName, *nebula.HostName]
ipNetFlag = textUnmarshalerFlag[nebula.IPNet, *nebula.IPNet] ipNetFlag = textUnmarshalerFlag[nebula.IPNet, *nebula.IPNet]
ipFlag = textUnmarshalerFlag[netip.Addr, *netip.Addr] ipFlag = textUnmarshalerFlag[netip.Addr, *netip.Addr]
) )
////////////////////////////////////////////////////////////////////////////////
type (
addrFlagStr string
addrFlag = textUnmarshalerFlag[addrFlagStr, *addrFlagStr]
)
func (f addrFlagStr) MarshalText() ([]byte, error) {
return []byte(f), nil
}
func (f *addrFlagStr) UnmarshalText(b []byte) error {
str := string(b)
_, portStr, err := net.SplitHostPort(str)
if err != nil {
return err
}
if _, err := strconv.ParseUint(portStr, 10, 16); err != nil {
return fmt.Errorf("invalid port %q", portStr)
}
*f = addrFlagStr(str)
return nil
}
////////////////////////////////////////////////////////////////////////////////
type logLevelFlag struct { type logLevelFlag struct {
mlog.Level mlog.Level
} }

View File

@ -3,6 +3,8 @@ package main
import ( import (
"bytes" "bytes"
"context" "context"
"fmt"
"isle/bootstrap"
"isle/daemon" "isle/daemon"
"isle/daemon/jsonrpc2" "isle/daemon/jsonrpc2"
"isle/toolkit" "isle/toolkit"
@ -15,6 +17,14 @@ import (
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
func bootstrapNewCreationParams(name, domain string) bootstrap.CreationParams {
return bootstrap.CreationParams{
ID: fmt.Sprintf("%s-%s", name, domain),
Name: name,
Domain: domain,
}
}
type runHarness struct { type runHarness struct {
ctx context.Context ctx context.Context
logger *mlog.Logger logger *mlog.Logger
@ -59,10 +69,11 @@ func (h *runHarness) run(t *testing.T, args ...string) error {
) )
return doRootCmd(h.ctx, h.logger, &subCmdCtxOpts{ return doRootCmd(h.ctx, h.logger, &subCmdCtxOpts{
args: args, args: args,
stdout: h.stdout, stdout: h.stdout,
changeStager: h.changeStager, changeStager: h.changeStager,
daemonRPC: daemonRPCClient, daemonRPC: daemonRPCClient,
bootstrapNewCreationParams: bootstrapNewCreationParams,
}) })
} }

View File

@ -6,10 +6,15 @@ import (
"fmt" "fmt"
"isle/bootstrap" "isle/bootstrap"
"isle/daemon" "isle/daemon"
"isle/daemon/daecommon"
"isle/daemon/network" "isle/daemon/network"
"isle/jsonutil" "isle/jsonutil"
"isle/nebula" "isle/nebula"
"isle/toolkit"
"path/filepath"
"slices" "slices"
"strconv"
"strings"
) )
var subCmdNetworkCreate = subCmd{ var subCmdNetworkCreate = subCmd{
@ -17,8 +22,9 @@ var subCmdNetworkCreate = subCmd{
descr: "Create's a new network, with this host being the first host in that network.", descr: "Create's a new network, with this host being the first host in that network.",
do: func(ctx subCmdCtx) error { do: func(ctx subCmdCtx) error {
var ( var (
ipNet ipNetFlag ipNet ipNetFlag
hostName hostNameFlag hostName hostNameFlag
vpnPublicAddr addrFlag
) )
name := ctx.flags.StringP( name := ctx.flags.StringP(
@ -44,6 +50,19 @@ var subCmdNetworkCreate = subCmd{
"Name of this host, which will be the first host in the network", "Name of this host, which will be the first host in the network",
) )
vpnPublicAddrF := ctx.flags.VarPF(
&vpnPublicAddr,
"vpn-public-address",
"",
"Public address (host:port) that this host is publicly available on",
)
storageAllocStrs := ctx.flags.StringArray(
"storage-allocation",
nil,
"Storage allocation on this host, in the form '<capacity-in-gb>@<path>`",
)
ctx, err := ctx.withParsedFlags(&withParsedFlagsOpts{ ctx, err := ctx.withParsedFlags(&withParsedFlagsOpts{
noNetwork: true, noNetwork: true,
}) })
@ -58,14 +77,61 @@ var subCmdNetworkCreate = subCmd{
return errors.New("--name, --domain, --ip-net, and --hostname are required") return errors.New("--name, --domain, --ip-net, and --hostname are required")
} }
type storageAlloc struct {
capacity uint64
path string
}
storageAllocs := make([]storageAlloc, len(*storageAllocStrs))
for i, str := range *storageAllocStrs {
capStr, path, ok := strings.Cut(str, "@")
if !ok {
return fmt.Errorf(
"malformed --storage-allocation %q, no '@' found", str,
)
}
capacity, err := strconv.ParseUint(capStr, 10, 64)
if err != nil {
return fmt.Errorf(
"invalid --storage-allocation capacity %q", capStr,
)
}
storageAllocs[i] = storageAlloc{capacity, path}
}
daemonRPC, err := ctx.newDaemonRPC() daemonRPC, err := ctx.newDaemonRPC()
if err != nil { if err != nil {
return fmt.Errorf("creating daemon RPC client: %w", err) return fmt.Errorf("creating daemon RPC client: %w", err)
} }
defer daemonRPC.Close() defer daemonRPC.Close()
var networkConfig *daecommon.NetworkConfig
if vpnPublicAddrF.Changed || len(storageAllocs) > 0 {
networkConfig = toolkit.Ptr(
daecommon.NewNetworkConfig(func(c *daecommon.NetworkConfig) {
c.VPN.PublicAddr = string(vpnPublicAddr.V)
for _, a := range storageAllocs {
c.Storage.Allocations = append(
c.Storage.Allocations,
daecommon.ConfigStorageAllocation{
DataPath: filepath.Join(a.path, "data"),
MetaPath: filepath.Join(a.path, "meta"),
Capacity: int(a.capacity),
},
)
}
}),
)
}
err = daemonRPC.CreateNetwork( err = daemonRPC.CreateNetwork(
ctx, *name, *domain, ipNet.V, hostName.V, ctx,
ctx.opts.bootstrapNewCreationParams(*name, *domain),
ipNet.V,
hostName.V,
&daemon.CreateNetworkOpts{Config: networkConfig},
) )
if err != nil { if err != nil {
return fmt.Errorf("creating network: %w", err) return fmt.Errorf("creating network: %w", err)

View File

@ -5,14 +5,133 @@ import (
"fmt" "fmt"
"isle/bootstrap" "isle/bootstrap"
"isle/daemon" "isle/daemon"
"isle/daemon/daecommon"
"isle/nebula" "isle/nebula"
"isle/toolkit" "isle/toolkit"
"net/netip" "net/netip"
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestNetworkCreate(t *testing.T) {
t.Parallel()
tests := []struct {
name string
expect func(*testing.T, *daemon.MockRPC)
flags []string
}{
{
name: "no given config",
expect: func(t *testing.T, daemonRPC *daemon.MockRPC) {
daemonRPC.
On(
"CreateNetwork",
toolkit.MockArg[context.Context](),
bootstrapNewCreationParams("aaa", "a.com"),
nebula.MustParseIPNet(t, "172.16.1.0/24"),
nebula.HostName("foo"),
&daemon.CreateNetworkOpts{},
).
Return(nil).
Once()
},
flags: []string{
"--name=aaa",
"--domain=a.com",
"--ip-net=172.16.1.0/24",
"--hostname=foo",
},
},
{
name: "partially given config",
expect: func(t *testing.T, daemonRPC *daemon.MockRPC) {
networkConfig := daecommon.NewNetworkConfig(func(c *daecommon.NetworkConfig) {
c.VPN.PublicAddr = "1.2.3.4:5"
})
daemonRPC.
On(
"CreateNetwork",
toolkit.MockArg[context.Context](),
bootstrapNewCreationParams("aaa", "a.com"),
nebula.MustParseIPNet(t, "172.16.1.0/24"),
nebula.HostName("foo"),
&daemon.CreateNetworkOpts{
Config: &networkConfig,
},
).
Return(nil).
Once()
},
flags: []string{
"--name=aaa",
"--domain=a.com",
"--ip-net=172.16.1.0/24",
"--hostname=foo",
"--vpn-public-address=1.2.3.4:5",
},
},
{
name: "fully given config",
expect: func(t *testing.T, daemonRPC *daemon.MockRPC) {
networkConfig := daecommon.NewNetworkConfig(func(c *daecommon.NetworkConfig) {
c.VPN.PublicAddr = "1.2.3.4:5"
c.Storage.Allocations = []daecommon.ConfigStorageAllocation{
{
DataPath: "/a/data",
MetaPath: "/a/meta",
Capacity: 100,
},
{
DataPath: "/b/data",
MetaPath: "/b/meta",
Capacity: 200,
},
}
})
daemonRPC.
On(
"CreateNetwork",
toolkit.MockArg[context.Context](),
bootstrapNewCreationParams("aaa", "a.com"),
nebula.MustParseIPNet(t, "172.16.1.0/24"),
nebula.HostName("foo"),
&daemon.CreateNetworkOpts{
Config: &networkConfig,
},
).
Return(nil).
Once()
},
flags: []string{
"--name=aaa",
"--domain=a.com",
"--ip-net=172.16.1.0/24",
"--hostname=foo",
"--vpn-public-address=1.2.3.4:5",
"--storage-allocation=100@/a",
"--storage-allocation=200@/b",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var (
h = newRunHarness(t)
args = append([]string{"network", "create"}, test.flags...)
)
test.expect(t, h.daemonRPC)
assert.NoError(t, h.run(t, args...))
})
}
}
func TestNetworkList(t *testing.T) { func TestNetworkList(t *testing.T) {
t.Parallel() t.Parallel()

View File

@ -5,6 +5,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"isle/bootstrap"
"isle/daemon" "isle/daemon"
"isle/daemon/jsonrpc2" "isle/daemon/jsonrpc2"
"isle/jsonutil" "isle/jsonutil"
@ -40,6 +41,9 @@ type subCmdCtxOpts struct {
stdout io.Writer stdout io.Writer
changeStager *changeStager changeStager *changeStager
daemonRPC daemon.RPC daemonRPC daemon.RPC
// defaults to bootstrap.NewCreationParams
bootstrapNewCreationParams func(name, domain string) bootstrap.CreationParams
} }
func (o *subCmdCtxOpts) withDefaults() *subCmdCtxOpts { func (o *subCmdCtxOpts) withDefaults() *subCmdCtxOpts {
@ -55,6 +59,10 @@ func (o *subCmdCtxOpts) withDefaults() *subCmdCtxOpts {
o.stdout = os.Stdout o.stdout = os.Stdout
} }
if o.bootstrapNewCreationParams == nil {
o.bootstrapNewCreationParams = bootstrap.NewCreationParams
}
return o return o
} }

View File

@ -3,7 +3,6 @@ package main
import ( import (
"errors" "errors"
"fmt" "fmt"
"net"
) )
var subCmdVPNPublicAddressGet = subCmd{ var subCmdVPNPublicAddressGet = subCmd{
@ -38,8 +37,11 @@ var subCmdVPNPublicAddressSet = subCmd{
name: "set", name: "set",
descr: "Set the public address of the host, or overwrite the already configured one", descr: "Set the public address of the host, or overwrite the already configured one",
do: func(ctx subCmdCtx) error { do: func(ctx subCmdCtx) error {
publicAddr := ctx.flags.String( var publicAddr addrFlag
"public-addr",
publicAddrF := ctx.flags.VarPF(
&publicAddr,
"to",
"", "",
"Public address (host:port) that this host is publicly available on", "Public address (host:port) that this host is publicly available on",
) )
@ -49,10 +51,8 @@ var subCmdVPNPublicAddressSet = subCmd{
return fmt.Errorf("parsing flags: %w", err) return fmt.Errorf("parsing flags: %w", err)
} }
if *publicAddr == "" { if !publicAddrF.Changed {
return errors.New("--public-addr is required") return errors.New("--public-addr is required")
} else if _, _, err := net.SplitHostPort(*publicAddr); err != nil {
return fmt.Errorf("invalid --public-addr: %w", err)
} }
daemonRPC, err := ctx.newDaemonRPC() daemonRPC, err := ctx.newDaemonRPC()
@ -66,7 +66,7 @@ var subCmdVPNPublicAddressSet = subCmd{
return fmt.Errorf("getting network config: %w", err) return fmt.Errorf("getting network config: %w", err)
} }
config.VPN.PublicAddr = *publicAddr config.VPN.PublicAddr = string(publicAddr.V)
return daemonRPC.SetConfig(ctx, config) return daemonRPC.SetConfig(ctx, config)
}, },

View File

@ -47,15 +47,15 @@ func (c *rpcClient) CreateNebulaCertificate(ctx context.Context, h1 nebula.HostN
return return
} }
func (c *rpcClient) CreateNetwork(ctx context.Context, name string, domain string, ipNet nebula.IPNet, hostName nebula.HostName) (err error) { func (c *rpcClient) CreateNetwork(ctx context.Context, creationParams bootstrap.CreationParams, ipNet nebula.IPNet, hostName nebula.HostName, opts *CreateNetworkOpts) (err error) {
err = c.client.Call( err = c.client.Call(
ctx, ctx,
nil, nil,
"CreateNetwork", "CreateNetwork",
name, creationParams,
domain,
ipNet, ipNet,
hostName, hostName,
opts,
) )
return return
} }

View File

@ -21,7 +21,8 @@ var _ RPC = (*Daemon)(nil)
type joinedNetwork struct { type joinedNetwork struct {
id string id string
network.Network network.Network
config *daecommon.NetworkConfig creationParams bootstrap.CreationParams
config *daecommon.NetworkConfig
} }
// Daemon implements all methods of the Daemon interface, plus others used // Daemon implements all methods of the Daemon interface, plus others used
@ -96,7 +97,7 @@ func New(
return nil, fmt.Errorf("loading network %q: %w", id, err) return nil, fmt.Errorf("loading network %q: %w", id, err)
} }
d.networks[id] = joinedNetwork{id, n, networkConfig} d.networks[id] = joinedNetwork{id, n, creationParams, networkConfig}
} }
return d, nil return d, nil
@ -114,26 +115,37 @@ func New(
// and will have full administrative privileges. // and will have full administrative privileges.
// //
// Errors: // Errors:
// - network.ErrInvalidConfig // - [network.ErrInvalidConfig]
// - ErrAlreadyJoined // - [ErrAlreadyJoined]
// - [ErrManagedNetworkConfig] - if `opts.NetworkConfig` is given, but a
// NetworkConfig is also provided in the Daemon's Config.
func (d *Daemon) CreateNetwork( func (d *Daemon) CreateNetwork(
ctx context.Context, ctx context.Context,
name, domain string, ipNet nebula.IPNet, hostName nebula.HostName, creationParams bootstrap.CreationParams,
ipNet nebula.IPNet,
hostName nebula.HostName,
opts *CreateNetworkOpts,
) error { ) error {
opts = opts.withDefaults()
d.l.Lock() d.l.Lock()
defer d.l.Unlock() defer d.l.Unlock()
var ( var (
creationParams = bootstrap.NewCreationParams(name, domain) networkConfig = pickNetworkConfig(
networkConfig = pickNetworkConfig(
d.daemonConfig.Networks, creationParams, d.daemonConfig.Networks, creationParams,
) )
networkLogger = networkLogger(d.logger, creationParams) networkLogger = networkLogger(d.logger, creationParams)
) )
if joined, err := alreadyJoined(ctx, d.networks, creationParams); err != nil { if opts.Config != nil {
return fmt.Errorf("checking if already joined to network: %w", err) if networkConfig != nil {
} else if joined { return ErrManagedNetworkConfig
}
networkConfig = opts.Config
}
if alreadyJoined(d.networks, creationParams) {
return ErrAlreadyJoined return ErrAlreadyJoined
} }
@ -154,7 +166,7 @@ func (d *Daemon) CreateNetwork(
networkLogger.Info(ctx, "Network created successfully") networkLogger.Info(ctx, "Network created successfully")
d.networks[creationParams.ID] = joinedNetwork{ d.networks[creationParams.ID] = joinedNetwork{
creationParams.ID, n, networkConfig, creationParams.ID, n, creationParams, networkConfig,
} }
return nil return nil
} }
@ -181,9 +193,7 @@ func (d *Daemon) JoinNetwork(
) )
) )
if joined, err := alreadyJoined(ctx, d.networks, creationParams); err != nil { if alreadyJoined(d.networks, creationParams) {
return fmt.Errorf("checking if already joined to network: %w", err)
} else if joined {
return ErrAlreadyJoined return ErrAlreadyJoined
} }
@ -203,7 +213,9 @@ func (d *Daemon) JoinNetwork(
} }
networkLogger.Info(ctx, "Network joined successfully") networkLogger.Info(ctx, "Network joined successfully")
d.networks[networkID] = joinedNetwork{networkID, n, networkConfig} d.networks[networkID] = joinedNetwork{
networkID, n, creationParams, networkConfig,
}
return nil return nil
} }

View File

@ -5,6 +5,7 @@ import (
"isle/bootstrap" "isle/bootstrap"
"isle/daemon/daecommon" "isle/daemon/daecommon"
"isle/daemon/network" "isle/daemon/network"
"isle/nebula"
"isle/toolkit" "isle/toolkit"
"testing" "testing"
@ -234,6 +235,134 @@ func TestNew(t *testing.T) {
}) })
} }
func TestDaemon_CreateNetwork(t *testing.T) {
t.Run("ErrManagedNetworkConfig", func(t *testing.T) {
var (
networkConfig = daecommon.NewNetworkConfig(nil)
h = newHarness(t, &harnessOpts{
config: daecommon.Config{
Networks: map[string]daecommon.NetworkConfig{
"AAA": networkConfig,
},
},
})
creationParams = bootstrap.NewCreationParams("AAA", "a.com")
ipNet = nebula.MustParseIPNet(t, "172.16.0.0/24")
)
err := h.daemon.CreateNetwork(
h.ctx,
creationParams,
ipNet,
nebula.HostName("foo"),
&CreateNetworkOpts{Config: &networkConfig},
)
assert.ErrorIs(t, err, ErrManagedNetworkConfig)
})
t.Run("ErrAlreadyJoined", func(t *testing.T) {
var (
creationParams = bootstrap.NewCreationParams("AAA", "a.com")
networkConfig = daecommon.NewNetworkConfig(nil)
h = newHarness(t, &harnessOpts{
expectNetworksLoaded: []expectNetworkLoad{{
creationParams, nil, network.NewMockNetwork(t),
}},
expectStoredConfigs: map[string]daecommon.NetworkConfig{
creationParams.ID: networkConfig,
},
})
ipNet = nebula.MustParseIPNet(t, "172.16.0.0/24")
)
err := h.daemon.CreateNetwork(
h.ctx,
bootstrap.NewCreationParams("AAA", "aaa.com"),
ipNet,
nebula.HostName("foo"),
nil,
)
assert.ErrorIs(t, err, ErrAlreadyJoined)
})
t.Run("success/config given", func(t *testing.T) {
networkA := network.NewMockNetwork(t)
networkA.On("Shutdown").Return(nil).Once()
var (
h = newHarness(t, nil)
creationParams = bootstrap.NewCreationParams("AAA", "a.com")
networkConfig = daecommon.NewNetworkConfig(func(c *daecommon.NetworkConfig) {
c.VPN.PublicAddr = "1.2.3.4:50"
})
ipNet = nebula.MustParseIPNet(t, "172.16.0.0/24")
hostName = nebula.HostName("foo")
)
h.networkLoader.
On(
"Create",
toolkit.MockArg[context.Context](),
toolkit.MockArg[*mlog.Logger](),
creationParams,
ipNet,
hostName,
&network.Opts{
Config: &networkConfig,
},
).
Return(networkA, nil).
Once()
err := h.daemon.CreateNetwork(
h.ctx,
creationParams,
ipNet,
hostName,
&CreateNetworkOpts{
Config: &networkConfig,
},
)
assert.NoError(t, err)
assert.Contains(t, h.daemon.networks, creationParams.ID)
})
t.Run("success/no config given", func(t *testing.T) {
networkA := network.NewMockNetwork(t)
networkA.On("Shutdown").Return(nil).Once()
var (
h = newHarness(t, nil)
creationParams = bootstrap.NewCreationParams("AAA", "a.com")
ipNet = nebula.MustParseIPNet(t, "172.16.0.0/24")
hostName = nebula.HostName("foo")
)
h.networkLoader.
On(
"Create",
toolkit.MockArg[context.Context](),
toolkit.MockArg[*mlog.Logger](),
creationParams,
ipNet,
hostName,
&network.Opts{},
).
Return(networkA, nil).
Once()
err := h.daemon.CreateNetwork(
h.ctx,
creationParams,
ipNet,
hostName,
nil,
)
assert.NoError(t, err)
assert.Contains(t, h.daemon.networks, creationParams.ID)
})
}
func TestDaemon_SetConfig(t *testing.T) { func TestDaemon_SetConfig(t *testing.T) {
t.Run("success", func(t *testing.T) { t.Run("success", func(t *testing.T) {
var ( var (

View File

@ -58,22 +58,13 @@ func pickNetwork(
} }
func alreadyJoined( func alreadyJoined(
ctx context.Context, networks map[string]joinedNetwork, creationParams bootstrap.CreationParams,
networks map[string]joinedNetwork, ) bool {
creationParams bootstrap.CreationParams, for _, network := range networks {
) ( if network.creationParams.Conflicts(creationParams) {
bool, error, return true
) {
for networkID, network := range networks {
existingCreationParams, err := network.GetNetworkCreationParams(ctx)
if err != nil {
return false, fmt.Errorf(
"getting creation params of network %q: %w", networkID, err,
)
} else if existingCreationParams.Conflicts(creationParams) {
return true, nil
} }
} }
return false, nil return false
} }

View File

@ -14,16 +14,31 @@ import (
"dev.mediocregopher.com/mediocre-go-lib.git/mlog" "dev.mediocregopher.com/mediocre-go-lib.git/mlog"
) )
// CreateNetworkOpts are optional arguments to the CreateNetwork method. A nil
// value is equivalent to a zero value.
type CreateNetworkOpts struct {
// Config will be used as the NetworkConfig for the new Network, rather than
// picking one provided in the Daemon's Config.
Config *daecommon.NetworkConfig
}
func (o *CreateNetworkOpts) withDefaults() *CreateNetworkOpts {
if o == nil {
o = new(CreateNetworkOpts)
}
return o
}
// RPC defines the methods which the Daemon exposes over RPC (via the RPCHandler // RPC defines the methods which the Daemon exposes over RPC (via the RPCHandler
// or HTTPHandler methods). Method documentation can be found on the Daemon // or HTTPHandler methods). Method documentation can be found on the Daemon
// type. // type.
type RPC interface { type RPC interface {
CreateNetwork( CreateNetwork(
ctx context.Context, ctx context.Context,
name string, creationParams bootstrap.CreationParams,
domain string,
ipNet nebula.IPNet, ipNet nebula.IPNet,
hostName nebula.HostName, hostName nebula.HostName,
opts *CreateNetworkOpts,
) error ) error
JoinNetwork(context.Context, network.JoiningBootstrap) error JoinNetwork(context.Context, network.JoiningBootstrap) error

View File

@ -76,17 +76,17 @@ func (_m *MockRPC) CreateNebulaCertificate(_a0 context.Context, _a1 nebula.HostN
return r0, r1 return r0, r1
} }
// CreateNetwork provides a mock function with given fields: ctx, name, domain, ipNet, hostName // CreateNetwork provides a mock function with given fields: ctx, creationParams, ipNet, hostName, opts
func (_m *MockRPC) CreateNetwork(ctx context.Context, name string, domain string, ipNet nebula.IPNet, hostName nebula.HostName) error { func (_m *MockRPC) CreateNetwork(ctx context.Context, creationParams bootstrap.CreationParams, ipNet nebula.IPNet, hostName nebula.HostName, opts *CreateNetworkOpts) error {
ret := _m.Called(ctx, name, domain, ipNet, hostName) ret := _m.Called(ctx, creationParams, ipNet, hostName, opts)
if len(ret) == 0 { if len(ret) == 0 {
panic("no return value specified for CreateNetwork") panic("no return value specified for CreateNetwork")
} }
var r0 error var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, nebula.IPNet, nebula.HostName) error); ok { if rf, ok := ret.Get(0).(func(context.Context, bootstrap.CreationParams, nebula.IPNet, nebula.HostName, *CreateNetworkOpts) error); ok {
r0 = rf(ctx, name, domain, ipNet, hostName) r0 = rf(ctx, creationParams, ipNet, hostName, opts)
} else { } else {
r0 = ret.Error(0) r0 = ret.Error(0)
} }

View File

@ -4,11 +4,21 @@ import (
"fmt" "fmt"
"net" "net"
"net/netip" "net/netip"
"testing"
) )
// IPNet is the CIDR of a nebula network. // IPNet is the CIDR of a nebula network.
type IPNet net.IPNet type IPNet net.IPNet
// MustParseIPNet is a test helper for parsing a string into an IPNet.
func MustParseIPNet(t *testing.T, str string) IPNet {
var ipNet IPNet
if err := ipNet.UnmarshalText([]byte(str)); err != nil {
t.Fatal(err)
}
return ipNet
}
// UnmarshalText parses and validates an IPNet from a text string. // UnmarshalText parses and validates an IPNet from a text string.
func (n *IPNet) UnmarshalText(b []byte) error { func (n *IPNet) UnmarshalText(b []byte) error {
str := string(b) str := string(b)

View File

@ -10,3 +10,8 @@ func IsZero[T any](v T) bool {
var zero T var zero T
return reflect.DeepEqual(v, zero) return reflect.DeepEqual(v, zero)
} }
// Ptr returns a pointer to the given value.
func Ptr[T any](v T) *T {
return &v
}

View File

@ -0,0 +1,12 @@
---
type: task
---
# Implement `storage allocation modify` Sub-Command
It should be possible to modify an allocation from the command-line, including
changing its capacity, location, and port numbers.
Location will be the difficult one. It will require shutting down the process,
copying the data from the old location, starting the process back up, and either
removing or renaming the old data to mark it as being a backup.

View File

@ -1,7 +1,5 @@
--- ---
type: task type: task
after:
- code/**
--- ---
# Update Documentation # Update Documentation
@ -11,3 +9,13 @@ updated.
Check through all development documentation, especially that surrounding Check through all development documentation, especially that surrounding
testing. testing.
Doc changes to included are:
- New page on network configuration, how it can be done via the `daemon.yml`
file or via the command-line (but not both!)
- Rework "Contributing a Lighthouse" so that it doesn't directly mention nebula
or lighthouses at all.
- Remove nebula reference from `daemon.yml` comments.