Fix bug in nebula TUN device naming, causing it to force nebula to reload too much
This commit is contained in:
parent
d2c16573ff
commit
c08b225ee2
@ -23,10 +23,11 @@ import (
|
|||||||
// - dnsmasq
|
// - dnsmasq
|
||||||
// - garage (0 or more, depending on configured storage allocations)
|
// - garage (0 or more, depending on configured storage allocations)
|
||||||
type Children struct {
|
type Children struct {
|
||||||
logger *mlog.Logger
|
logger *mlog.Logger
|
||||||
binDirPath string
|
binDirPath string
|
||||||
runtimeDir toolkit.Dir
|
runtimeDir toolkit.Dir
|
||||||
garageAdminToken string
|
garageAdminToken string
|
||||||
|
nebulaDeviceNamer *NebulaDeviceNamer
|
||||||
|
|
||||||
garageRPCSecret string
|
garageRPCSecret string
|
||||||
|
|
||||||
@ -45,6 +46,7 @@ func New(
|
|||||||
networkConfig daecommon.NetworkConfig,
|
networkConfig daecommon.NetworkConfig,
|
||||||
runtimeDir toolkit.Dir,
|
runtimeDir toolkit.Dir,
|
||||||
garageAdminToken string,
|
garageAdminToken string,
|
||||||
|
nebulaDeviceNamer *NebulaDeviceNamer,
|
||||||
hostBootstrap bootstrap.Bootstrap,
|
hostBootstrap bootstrap.Bootstrap,
|
||||||
) (
|
) (
|
||||||
*Children, error,
|
*Children, error,
|
||||||
@ -56,11 +58,12 @@ func New(
|
|||||||
}
|
}
|
||||||
|
|
||||||
c := &Children{
|
c := &Children{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
binDirPath: binDirPath,
|
binDirPath: binDirPath,
|
||||||
runtimeDir: runtimeDir,
|
runtimeDir: runtimeDir,
|
||||||
garageAdminToken: garageAdminToken,
|
garageAdminToken: garageAdminToken,
|
||||||
garageRPCSecret: garageRPCSecret,
|
nebulaDeviceNamer: nebulaDeviceNamer,
|
||||||
|
garageRPCSecret: garageRPCSecret,
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.nebulaProc, err = nebulaPmuxProc(
|
if c.nebulaProc, err = nebulaPmuxProc(
|
||||||
@ -68,6 +71,7 @@ func New(
|
|||||||
c.logger,
|
c.logger,
|
||||||
c.runtimeDir.Path,
|
c.runtimeDir.Path,
|
||||||
c.binDirPath,
|
c.binDirPath,
|
||||||
|
c.nebulaDeviceNamer,
|
||||||
networkConfig,
|
networkConfig,
|
||||||
hostBootstrap,
|
hostBootstrap,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
@ -145,7 +149,12 @@ func (c *Children) reloadNebula(
|
|||||||
hostBootstrap bootstrap.Bootstrap,
|
hostBootstrap bootstrap.Bootstrap,
|
||||||
) error {
|
) error {
|
||||||
if _, changed, err := nebulaWriteConfig(
|
if _, changed, err := nebulaWriteConfig(
|
||||||
ctx, c.logger, c.runtimeDir.Path, networkConfig, hostBootstrap,
|
ctx,
|
||||||
|
c.logger,
|
||||||
|
c.runtimeDir.Path,
|
||||||
|
c.nebulaDeviceNamer,
|
||||||
|
networkConfig,
|
||||||
|
hostBootstrap,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return fmt.Errorf("writing a new nebula config: %w", err)
|
return fmt.Errorf("writing a new nebula config: %w", err)
|
||||||
} else if !changed {
|
} else if !changed {
|
||||||
|
@ -6,7 +6,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"isle/bootstrap"
|
"isle/bootstrap"
|
||||||
"isle/daemon/daecommon"
|
"isle/daemon/daecommon"
|
||||||
"isle/nebula"
|
|
||||||
"isle/toolkit"
|
"isle/toolkit"
|
||||||
"isle/yamlutil"
|
"isle/yamlutil"
|
||||||
"net"
|
"net"
|
||||||
@ -53,6 +52,7 @@ func waitForNebula(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func nebulaConfig(
|
func nebulaConfig(
|
||||||
|
deviceNamer *NebulaDeviceNamer,
|
||||||
networkConfig daecommon.NetworkConfig,
|
networkConfig daecommon.NetworkConfig,
|
||||||
hostBootstrap bootstrap.Bootstrap,
|
hostBootstrap bootstrap.Bootstrap,
|
||||||
) (
|
) (
|
||||||
@ -119,7 +119,10 @@ func nebulaConfig(
|
|||||||
"respond": true,
|
"respond": true,
|
||||||
},
|
},
|
||||||
"tun": m{
|
"tun": m{
|
||||||
"dev": nebula.GetDeviceName(hostBootstrap.NetworkCreationParams.ID),
|
"dev": deviceNamer.getName(
|
||||||
|
hostBootstrap.NetworkCreationParams.ID,
|
||||||
|
hostBootstrap.ThisHost().IP(),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
"firewall": firewall,
|
"firewall": firewall,
|
||||||
}
|
}
|
||||||
@ -169,12 +172,13 @@ func nebulaWriteConfig(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
logger *mlog.Logger,
|
logger *mlog.Logger,
|
||||||
runtimeDirPath string,
|
runtimeDirPath string,
|
||||||
|
deviceNamer *NebulaDeviceNamer,
|
||||||
networkConfig daecommon.NetworkConfig,
|
networkConfig daecommon.NetworkConfig,
|
||||||
hostBootstrap bootstrap.Bootstrap,
|
hostBootstrap bootstrap.Bootstrap,
|
||||||
) (
|
) (
|
||||||
string, bool, error,
|
string, bool, error,
|
||||||
) {
|
) {
|
||||||
config, err := nebulaConfig(networkConfig, hostBootstrap)
|
config, err := nebulaConfig(deviceNamer, networkConfig, hostBootstrap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", false, fmt.Errorf("creating nebula config: %w", err)
|
return "", false, fmt.Errorf("creating nebula config: %w", err)
|
||||||
}
|
}
|
||||||
@ -199,13 +203,14 @@ func nebulaPmuxProc(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
logger *mlog.Logger,
|
logger *mlog.Logger,
|
||||||
runtimeDirPath, binDirPath string,
|
runtimeDirPath, binDirPath string,
|
||||||
|
deviceNamer *NebulaDeviceNamer,
|
||||||
networkConfig daecommon.NetworkConfig,
|
networkConfig daecommon.NetworkConfig,
|
||||||
hostBootstrap bootstrap.Bootstrap,
|
hostBootstrap bootstrap.Bootstrap,
|
||||||
) (
|
) (
|
||||||
*pmuxlib.Process, error,
|
*pmuxlib.Process, error,
|
||||||
) {
|
) {
|
||||||
nebulaYmlPath, _, err := nebulaWriteConfig(
|
nebulaYmlPath, _, err := nebulaWriteConfig(
|
||||||
ctx, logger, runtimeDirPath, networkConfig, hostBootstrap,
|
ctx, logger, runtimeDirPath, deviceNamer, networkConfig, hostBootstrap,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("writing nebula config: %w", err)
|
return nil, fmt.Errorf("writing nebula config: %w", err)
|
||||||
|
51
go/daemon/children/nebula_device_namer.go
Normal file
51
go/daemon/children/nebula_device_namer.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package children
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type nebulaDeviceNamerKey struct {
|
||||||
|
networkID string
|
||||||
|
deviceIP netip.Addr
|
||||||
|
}
|
||||||
|
|
||||||
|
// NebulaDeviceNamer is used to assign unique TUN device names to different
|
||||||
|
// nebula networks running on the same host. It is intended to be shared amongst
|
||||||
|
// all running Children instances so that they do not accidentally conflict in
|
||||||
|
// TUN device names.
|
||||||
|
//
|
||||||
|
// NebulaDeviceNamer is thread-safe.
|
||||||
|
type NebulaDeviceNamer struct {
|
||||||
|
l sync.Mutex
|
||||||
|
counter uint64
|
||||||
|
assigned map[nebulaDeviceNamerKey]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNebulaDeviceNamer initializes and returns a new instance.
|
||||||
|
func NewNebulaDeviceNamer() *NebulaDeviceNamer {
|
||||||
|
return &NebulaDeviceNamer{
|
||||||
|
assigned: map[nebulaDeviceNamerKey]string{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getName returns a unique TUN device name for the given network and IP,
|
||||||
|
// generating a new one if the ID has never been given before.
|
||||||
|
func (n *NebulaDeviceNamer) getName(networkID string, ip netip.Addr) string {
|
||||||
|
key := nebulaDeviceNamerKey{networkID, ip}
|
||||||
|
|
||||||
|
n.l.Lock()
|
||||||
|
defer n.l.Unlock()
|
||||||
|
|
||||||
|
if name, ok := n.assigned[key]; ok {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
i := n.counter
|
||||||
|
n.counter++
|
||||||
|
|
||||||
|
name := fmt.Sprintf("isle%d-%s", i, networkID)
|
||||||
|
n.assigned[key] = name
|
||||||
|
return name
|
||||||
|
}
|
@ -6,6 +6,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"isle/bootstrap"
|
"isle/bootstrap"
|
||||||
|
"isle/daemon/children"
|
||||||
"isle/daemon/daecommon"
|
"isle/daemon/daecommon"
|
||||||
"isle/nebula"
|
"isle/nebula"
|
||||||
"isle/toolkit"
|
"isle/toolkit"
|
||||||
@ -126,6 +127,7 @@ type loader struct {
|
|||||||
envBinDirPath string
|
envBinDirPath string
|
||||||
networksStateDir toolkit.Dir
|
networksStateDir toolkit.Dir
|
||||||
networksRuntimeDir toolkit.Dir
|
networksRuntimeDir toolkit.Dir
|
||||||
|
nebulaDeviceNamer *children.NebulaDeviceNamer
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLoader returns a new Loader which will use the given directories to load
|
// NewLoader returns a new Loader which will use the given directories to load
|
||||||
@ -167,6 +169,7 @@ func NewLoader(
|
|||||||
envBinDirPath,
|
envBinDirPath,
|
||||||
networksStateDir,
|
networksStateDir,
|
||||||
networksRuntimeDir,
|
networksRuntimeDir,
|
||||||
|
children.NewNebulaDeviceNamer(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -222,6 +225,7 @@ func (l *loader) Load(
|
|||||||
ctx,
|
ctx,
|
||||||
logger.WithNamespace("network"),
|
logger.WithNamespace("network"),
|
||||||
l.envBinDirPath,
|
l.envBinDirPath,
|
||||||
|
l.nebulaDeviceNamer,
|
||||||
networkStateDir,
|
networkStateDir,
|
||||||
networkRuntimeDir,
|
networkRuntimeDir,
|
||||||
opts,
|
opts,
|
||||||
@ -254,6 +258,7 @@ func (l *loader) Join(
|
|||||||
ctx,
|
ctx,
|
||||||
logger.WithNamespace("network"),
|
logger.WithNamespace("network"),
|
||||||
l.envBinDirPath,
|
l.envBinDirPath,
|
||||||
|
l.nebulaDeviceNamer,
|
||||||
joiningBootstrap,
|
joiningBootstrap,
|
||||||
networkStateDir,
|
networkStateDir,
|
||||||
networkRuntimeDir,
|
networkRuntimeDir,
|
||||||
@ -286,6 +291,7 @@ func (l *loader) Create(
|
|||||||
ctx,
|
ctx,
|
||||||
logger.WithNamespace("network"),
|
logger.WithNamespace("network"),
|
||||||
l.envBinDirPath,
|
l.envBinDirPath,
|
||||||
|
l.nebulaDeviceNamer,
|
||||||
networkStateDir,
|
networkStateDir,
|
||||||
networkRuntimeDir,
|
networkRuntimeDir,
|
||||||
creationParams,
|
creationParams,
|
||||||
|
@ -166,9 +166,10 @@ func (o *Opts) withDefaults() *Opts {
|
|||||||
type network struct {
|
type network struct {
|
||||||
logger *mlog.Logger
|
logger *mlog.Logger
|
||||||
|
|
||||||
envBinDirPath string
|
envBinDirPath string
|
||||||
stateDir toolkit.Dir
|
nebulaDeviceNamer *children.NebulaDeviceNamer
|
||||||
runtimeDir toolkit.Dir
|
stateDir toolkit.Dir
|
||||||
|
runtimeDir toolkit.Dir
|
||||||
|
|
||||||
opts *Opts
|
opts *Opts
|
||||||
|
|
||||||
@ -191,6 +192,7 @@ func newNetwork(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
logger *mlog.Logger,
|
logger *mlog.Logger,
|
||||||
envBinDirPath string,
|
envBinDirPath string,
|
||||||
|
nebulaDeviceNamer *children.NebulaDeviceNamer,
|
||||||
stateDir toolkit.Dir,
|
stateDir toolkit.Dir,
|
||||||
runtimeDir toolkit.Dir,
|
runtimeDir toolkit.Dir,
|
||||||
dirsMayExist bool,
|
dirsMayExist bool,
|
||||||
@ -202,13 +204,14 @@ func newNetwork(
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
n = &network{
|
n = &network{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
envBinDirPath: envBinDirPath,
|
envBinDirPath: envBinDirPath,
|
||||||
stateDir: stateDir,
|
nebulaDeviceNamer: nebulaDeviceNamer,
|
||||||
runtimeDir: runtimeDir,
|
stateDir: stateDir,
|
||||||
opts: opts.withDefaults(),
|
runtimeDir: runtimeDir,
|
||||||
workerCtx: ctx,
|
opts: opts.withDefaults(),
|
||||||
workerCancel: cancel,
|
workerCtx: ctx,
|
||||||
|
workerCancel: cancel,
|
||||||
}
|
}
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
@ -253,6 +256,7 @@ func load(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
logger *mlog.Logger,
|
logger *mlog.Logger,
|
||||||
envBinDirPath string,
|
envBinDirPath string,
|
||||||
|
nebulaDeviceNamer *children.NebulaDeviceNamer,
|
||||||
stateDir toolkit.Dir,
|
stateDir toolkit.Dir,
|
||||||
runtimeDir toolkit.Dir,
|
runtimeDir toolkit.Dir,
|
||||||
opts *Opts,
|
opts *Opts,
|
||||||
@ -263,6 +267,7 @@ func load(
|
|||||||
ctx,
|
ctx,
|
||||||
logger,
|
logger,
|
||||||
envBinDirPath,
|
envBinDirPath,
|
||||||
|
nebulaDeviceNamer,
|
||||||
stateDir,
|
stateDir,
|
||||||
runtimeDir,
|
runtimeDir,
|
||||||
true,
|
true,
|
||||||
@ -292,6 +297,7 @@ func join(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
logger *mlog.Logger,
|
logger *mlog.Logger,
|
||||||
envBinDirPath string,
|
envBinDirPath string,
|
||||||
|
nebulaDeviceNamer *children.NebulaDeviceNamer,
|
||||||
joiningBootstrap JoiningBootstrap,
|
joiningBootstrap JoiningBootstrap,
|
||||||
stateDir toolkit.Dir,
|
stateDir toolkit.Dir,
|
||||||
runtimeDir toolkit.Dir,
|
runtimeDir toolkit.Dir,
|
||||||
@ -303,6 +309,7 @@ func join(
|
|||||||
ctx,
|
ctx,
|
||||||
logger,
|
logger,
|
||||||
envBinDirPath,
|
envBinDirPath,
|
||||||
|
nebulaDeviceNamer,
|
||||||
stateDir,
|
stateDir,
|
||||||
runtimeDir,
|
runtimeDir,
|
||||||
false,
|
false,
|
||||||
@ -329,6 +336,7 @@ func create(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
logger *mlog.Logger,
|
logger *mlog.Logger,
|
||||||
envBinDirPath string,
|
envBinDirPath string,
|
||||||
|
nebulaDeviceNamer *children.NebulaDeviceNamer,
|
||||||
stateDir toolkit.Dir,
|
stateDir toolkit.Dir,
|
||||||
runtimeDir toolkit.Dir,
|
runtimeDir toolkit.Dir,
|
||||||
creationParams bootstrap.CreationParams,
|
creationParams bootstrap.CreationParams,
|
||||||
@ -349,6 +357,7 @@ func create(
|
|||||||
ctx,
|
ctx,
|
||||||
logger,
|
logger,
|
||||||
envBinDirPath,
|
envBinDirPath,
|
||||||
|
nebulaDeviceNamer,
|
||||||
stateDir,
|
stateDir,
|
||||||
runtimeDir,
|
runtimeDir,
|
||||||
false,
|
false,
|
||||||
@ -461,6 +470,7 @@ func (n *network) initialize(
|
|||||||
n.networkConfig,
|
n.networkConfig,
|
||||||
n.runtimeDir,
|
n.runtimeDir,
|
||||||
n.opts.GarageAdminToken,
|
n.opts.GarageAdminToken,
|
||||||
|
n.nebulaDeviceNamer,
|
||||||
n.currBootstrap,
|
n.currBootstrap,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"isle/bootstrap"
|
"isle/bootstrap"
|
||||||
|
"isle/daemon/children"
|
||||||
"isle/daemon/daecommon"
|
"isle/daemon/daecommon"
|
||||||
"isle/garage"
|
"isle/garage"
|
||||||
"isle/nebula"
|
"isle/nebula"
|
||||||
@ -60,10 +61,11 @@ func newPublicAddr() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type integrationHarness struct {
|
type integrationHarness struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
logger *mlog.Logger
|
logger *mlog.Logger
|
||||||
rootDir toolkit.Dir
|
rootDir toolkit.Dir
|
||||||
dirCounter atomic.Uint64
|
dirCounter atomic.Uint64
|
||||||
|
nebulaDeviceNamer *children.NebulaDeviceNamer
|
||||||
}
|
}
|
||||||
|
|
||||||
func newIntegrationHarness(t *testing.T) *integrationHarness {
|
func newIntegrationHarness(t *testing.T) *integrationHarness {
|
||||||
@ -86,9 +88,10 @@ func newIntegrationHarness(t *testing.T) *integrationHarness {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return &integrationHarness{
|
return &integrationHarness{
|
||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
logger: toolkit.NewTestLogger(t),
|
logger: toolkit.NewTestLogger(t),
|
||||||
rootDir: toolkit.Dir{Path: rootDir},
|
rootDir: toolkit.Dir{Path: rootDir},
|
||||||
|
nebulaDeviceNamer: children.NewNebulaDeviceNamer(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,6 +173,7 @@ type integrationHarnessNetwork struct {
|
|||||||
logger *mlog.Logger
|
logger *mlog.Logger
|
||||||
hostName nebula.HostName
|
hostName nebula.HostName
|
||||||
stateDir, runtimeDir toolkit.Dir
|
stateDir, runtimeDir toolkit.Dir
|
||||||
|
nebulaDeviceNamer *children.NebulaDeviceNamer
|
||||||
opts *Opts
|
opts *Opts
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -204,6 +208,7 @@ func (h *integrationHarness) createNetwork(
|
|||||||
h.ctx,
|
h.ctx,
|
||||||
logger,
|
logger,
|
||||||
getEnvBinDirPath(),
|
getEnvBinDirPath(),
|
||||||
|
h.nebulaDeviceNamer,
|
||||||
stateDir,
|
stateDir,
|
||||||
runtimeDir,
|
runtimeDir,
|
||||||
opts.creationParams,
|
opts.creationParams,
|
||||||
@ -222,6 +227,7 @@ func (h *integrationHarness) createNetwork(
|
|||||||
hostName,
|
hostName,
|
||||||
stateDir,
|
stateDir,
|
||||||
runtimeDir,
|
runtimeDir,
|
||||||
|
h.nebulaDeviceNamer,
|
||||||
networkOpts,
|
networkOpts,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -286,6 +292,7 @@ func (h *integrationHarness) joinNetwork(
|
|||||||
h.ctx,
|
h.ctx,
|
||||||
logger,
|
logger,
|
||||||
getEnvBinDirPath(),
|
getEnvBinDirPath(),
|
||||||
|
h.nebulaDeviceNamer,
|
||||||
joiningBootstrap,
|
joiningBootstrap,
|
||||||
stateDir,
|
stateDir,
|
||||||
runtimeDir,
|
runtimeDir,
|
||||||
@ -302,6 +309,7 @@ func (h *integrationHarness) joinNetwork(
|
|||||||
hostName,
|
hostName,
|
||||||
stateDir,
|
stateDir,
|
||||||
runtimeDir,
|
runtimeDir,
|
||||||
|
h.nebulaDeviceNamer,
|
||||||
networkOpts,
|
networkOpts,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -327,6 +335,7 @@ func (nh *integrationHarnessNetwork) restart(t *testing.T) {
|
|||||||
nh.ctx,
|
nh.ctx,
|
||||||
nh.logger,
|
nh.logger,
|
||||||
getEnvBinDirPath(),
|
getEnvBinDirPath(),
|
||||||
|
nh.nebulaDeviceNamer,
|
||||||
nh.stateDir,
|
nh.stateDir,
|
||||||
nh.runtimeDir,
|
nh.runtimeDir,
|
||||||
nh.opts,
|
nh.opts,
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
package nebula
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sync/atomic"
|
|
||||||
)
|
|
||||||
|
|
||||||
var deviceCounter = new(atomic.Uint64)
|
|
||||||
|
|
||||||
// GetDeviceName returns the network device name to use for a particular
|
|
||||||
// network. Each returns name is gauranteed to be unique for the lifetime of the
|
|
||||||
// process.
|
|
||||||
func GetDeviceName(networkID string) string {
|
|
||||||
i := deviceCounter.Add(1) - 1
|
|
||||||
// the returned string will be too long for linux, but it will get
|
|
||||||
// automatically truncated.
|
|
||||||
return fmt.Sprintf("isle%d-%s", i, networkID)
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
type: tasks
|
|
||||||
---
|
|
||||||
|
|
||||||
# Address All TODOs
|
|
||||||
|
|
||||||
All TODOs which remain in the codebase must be addressed or moved into a task
|
|
||||||
file.
|
|
Loading…
Reference in New Issue
Block a user