Compare commits

..

7 Commits

Author SHA1 Message Date
64fdba0a48 Implement network(s) list sub-command 2024-09-24 11:03:18 +02:00
de7aac1f25 Refactor command-line parsing, pass --network to most commands 2024-09-23 20:50:45 +02:00
16aca610b4 Add multi-network support to daemon
It's still not possible to pick a network from the command-line, so this
is a bit broken, but the daemon component should handle it correctly at
least.
2024-09-23 19:04:14 +02:00
6c036d1183 Check that two different networks aren't trying to use the same nebula port 2024-09-12 08:59:23 +02:00
df4eae8a5c Support configuring more than one network 2024-09-10 22:51:33 +02:00
c022c97b19 include a migration for multi-network state directories 2024-09-10 21:02:07 +02:00
6d99fb5368 Remove randStr private utility function 2024-09-09 21:38:10 +02:00
41 changed files with 924 additions and 430 deletions

View File

@ -1,4 +1,3 @@
# #
# This file defines all configuration directives which can be modified for # This file defines all configuration directives which can be modified for
# the isle daemon at runtime. All values specified here are the # the isle daemon at runtime. All values specified here are the
@ -6,17 +5,23 @@
# #
################################################################################ ################################################################################
# A DNS service runs as part of every isle process. # Configuration broken down by network. Each network can be identified by its
dns: # ID, its name, or its domain.
#networks:
#id-or-name-or-domain:
# A DNS service runs as part of every isle process.
#dns:
# list of IPs that the DNS service will use to resolve requests outside the # list of IPs that the DNS service will use to resolve requests outside the
# network's domain. # network's domain.
resolvers: #resolvers:
- 1.1.1.1 # - 1.1.1.1
- 8.8.8.8 # - 8.8.8.8
# A VPN service runs as part of every isle process. # A VPN service runs as part of every isle process.
vpn: #vpn:
# Enable this field if the vpn will be made to be publicly accessible at a # Enable this field if the vpn will be made to be publicly accessible at a
# particular IP or hostname. At least one host must have a publicly accessible # particular IP or hostname. At least one host must have a publicly accessible
@ -25,38 +30,28 @@ vpn:
# Firewall directives, as described here: # Firewall directives, as described here:
# https://github.com/slackhq/nebula/blob/v1.6.1/examples/config.yml#L260 # https://github.com/slackhq/nebula/blob/v1.6.1/examples/config.yml#L260
firewall: #firewall:
conntrack:
tcp_timeout: 12m
udp_timeout: 3m
default_timeout: 10m
max_connections: 100000
outbound:
# Allow all outbound traffic from this node. # Allow all outbound traffic from this node.
- port: any #outbound:
proto: any # - port: any
host: any # proto: any
# host: any
inbound:
# If any storage allocations are declared below, the ports used will be
# allowed here automatically.
# Allow ICMP between hosts. # Allow ICMP between hosts.
- port: any #inbound:
proto: icmp # - port: any
host: any # proto: icmp
# host: any
#
# # If any storage allocations are declared below, the ports used will be
# # allowed here automatically.
# That's it. #tun:
tun:
# Name of the tun network device which will route VPN traffic. # Name of the tun network device which will route VPN traffic.
device: isle-tun #device: isle-tun
storage: #storage:
# Allocations defined here are used to store data in the distributed storage # Allocations defined here are used to store data in the distributed storage
# network. If no allocations are defined then no data is replicated to this # network. If no allocations are defined then no data is replicated to this

View File

@ -12,6 +12,7 @@ import (
"net/netip" "net/netip"
"path/filepath" "path/filepath"
"sort" "sort"
"strings"
"dev.mediocregopher.com/mediocre-go-lib.git/mctx" "dev.mediocregopher.com/mediocre-go-lib.git/mctx"
) )
@ -52,6 +53,42 @@ func (p CreationParams) Annotate(aa mctx.Annotations) {
aa["networkDomain"] = p.Domain aa["networkDomain"] = p.Domain
} }
// Matches returns true if the given string matches some aspect of the
// CreationParams.
func (p CreationParams) Matches(str string) bool {
if strings.HasPrefix(p.ID, str) {
return true
}
if strings.EqualFold(p.Name, str) {
return true
}
if strings.EqualFold(p.Domain, str) {
return true
}
return false
}
// Conflicts returns true if either CreationParams has some parameter which
// overlaps with that of the other.
func (p CreationParams) Conflicts(p2 CreationParams) bool {
if p.ID == p2.ID {
return true
}
if strings.EqualFold(p.Name, p2.Name) {
return true
}
if strings.EqualFold(p.Domain, p2.Domain) {
return true
}
return false
}
// Bootstrap contains all information which is needed by a host daemon to join a // Bootstrap contains all information which is needed by a host daemon to join a
// network on boot. // network on boot.
type Bootstrap struct { type Bootstrap struct {

View File

@ -6,7 +6,7 @@ import (
) )
func (ctx subCmdCtx) getHosts() ([]bootstrap.Host, error) { func (ctx subCmdCtx) getHosts() ([]bootstrap.Host, error) {
res, err := ctx.daemonRPC.GetHosts(ctx) res, err := newDaemonRPCClient().GetHosts(ctx)
if err != nil { if err != nil {
return nil, fmt.Errorf("calling GetHosts: %w", err) return nil, fmt.Errorf("calling GetHosts: %w", err)
} }

View File

@ -18,26 +18,25 @@ import (
var subCmdDaemon = subCmd{ var subCmdDaemon = subCmd{
name: "daemon", name: "daemon",
descr: "Runs the isle daemon (Default if no sub-command given)", descr: "Runs the isle daemon (Default if no sub-command given)",
noNetwork: true,
do: func(ctx subCmdCtx) error { do: func(ctx subCmdCtx) error {
daemonConfigPath := ctx.flags.StringP(
flags := ctx.flagSet(false)
daemonConfigPath := flags.StringP(
"config-path", "c", "", "config-path", "c", "",
"Optional path to a daemon.yml file to load configuration from.", "Optional path to a daemon.yml file to load configuration from.",
) )
dumpConfig := flags.Bool( dumpConfig := ctx.flags.Bool(
"dump-config", false, "dump-config", false,
"Write the default configuration file to stdout and exit.", "Write the default configuration file to stdout and exit.",
) )
logLevelStr := flags.StringP( logLevelStr := ctx.flags.StringP(
"log-level", "l", "info", "log-level", "l", "info",
`Maximum log level which should be output. Values can be "debug", "info", "warn", "error", "fatal". Does not apply to sub-processes`, `Maximum log level which should be output. Values can be "debug", "info", "warn", "error", "fatal". Does not apply to sub-processes`,
) )
if err := flags.Parse(ctx.args); err != nil { ctx, err := ctx.withParsedFlags()
if err != nil {
return fmt.Errorf("parsing flags: %w", err) return fmt.Errorf("parsing flags: %w", err)
} }
@ -56,7 +55,7 @@ var subCmdDaemon = subCmd{
// required linux capabilities are set. // required linux capabilities are set.
// TODO check that the tun module is loaded (for nebula). // TODO check that the tun module is loaded (for nebula).
daemonConfig, err := daecommon.LoadConfig(envAppDirPath, *daemonConfigPath) daemonConfig, err := daecommon.LoadConfig(*daemonConfigPath)
if err != nil { if err != nil {
return fmt.Errorf("loading daemon config: %w", err) return fmt.Errorf("loading daemon config: %w", err)
} }

View File

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"io/fs" "io/fs"
"isle/daemon" "isle/daemon"
"isle/daemon/jsonrpc2"
"net" "net"
"net/http" "net/http"
"os" "os"
@ -16,6 +17,14 @@ import (
const daemonHTTPRPCPath = "/rpc/v0.json" const daemonHTTPRPCPath = "/rpc/v0.json"
func newDaemonRPCClient() daemon.RPC {
return daemon.RPCFromClient(
jsonrpc2.NewUnixHTTPClient(
daemon.HTTPSocketPath(), daemonHTTPRPCPath,
),
)
}
func newHTTPServer( func newHTTPServer(
ctx context.Context, logger *mlog.Logger, daemonInst *daemon.Daemon, ctx context.Context, logger *mlog.Logger, daemonInst *daemon.Daemon,
) ( ) (

View File

@ -34,25 +34,24 @@ func initMCConfigDir(envVars daecommon.EnvVars) (string, error) {
var subCmdGarageMC = subCmd{ var subCmdGarageMC = subCmd{
name: "mc", name: "mc",
descr: "Runs the mc (minio-client) binary. The isle garage can be accessed under the `garage` alias", descr: "Runs the mc (minio-client) binary. The isle garage can be accessed under the `garage` alias",
passthroughArgs: true,
do: func(ctx subCmdCtx) error { do: func(ctx subCmdCtx) error {
keyID := ctx.flags.StringP(
flags := ctx.flagSet(true)
keyID := flags.StringP(
"key-id", "i", "", "key-id", "i", "",
"Optional key ID to use, defaults to that of the shared global key", "Optional key ID to use, defaults to that of the shared global key",
) )
keySecret := flags.StringP( keySecret := ctx.flags.StringP(
"key-secret", "s", "", "key-secret", "s", "",
"Optional key secret to use, defaults to that of the shared global key", "Optional key secret to use, defaults to that of the shared global key",
) )
if err := flags.Parse(ctx.args); err != nil { ctx, err := ctx.withParsedFlags()
if err != nil {
return fmt.Errorf("parsing flags: %w", err) return fmt.Errorf("parsing flags: %w", err)
} }
clientParams, err := ctx.daemonRPC.GetGarageClientParams(ctx) clientParams, err := newDaemonRPCClient().GetGarageClientParams(ctx)
if err != nil { if err != nil {
return fmt.Errorf("calling GetGarageClientParams: %w", err) return fmt.Errorf("calling GetGarageClientParams: %w", err)
} }
@ -67,9 +66,9 @@ var subCmdGarageMC = subCmd{
*keySecret = clientParams.GlobalBucketS3APICredentials.Secret *keySecret = clientParams.GlobalBucketS3APICredentials.Secret
} }
args := flags.Args() args := ctx.flags.Args()
if i := flags.ArgsLenAtDash(); i >= 0 { if i := ctx.flags.ArgsLenAtDash(); i >= 0 {
args = args[i:] args = args[i:]
} }
@ -117,8 +116,12 @@ var subCmdGarageCLI = subCmd{
name: "cli", name: "cli",
descr: "Runs the garage binary, automatically configured to point to the garage sub-process of a running isle daemon", descr: "Runs the garage binary, automatically configured to point to the garage sub-process of a running isle daemon",
do: func(ctx subCmdCtx) error { do: func(ctx subCmdCtx) error {
ctx, err := ctx.withParsedFlags()
if err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
clientParams, err := ctx.daemonRPC.GetGarageClientParams(ctx) clientParams, err := newDaemonRPCClient().GetGarageClientParams(ctx)
if err != nil { if err != nil {
return fmt.Errorf("calling GetGarageClientParams: %w", err) return fmt.Errorf("calling GetGarageClientParams: %w", err)
} }

View File

@ -16,26 +16,26 @@ var subCmdHostCreate = subCmd{
descr: "Creates a new host in the network, writing its new bootstrap.json to stdout", descr: "Creates a new host in the network, writing its new bootstrap.json to stdout",
do: func(ctx subCmdCtx) error { do: func(ctx subCmdCtx) error {
var ( var (
flags = ctx.flagSet(false)
hostName hostNameFlag hostName hostNameFlag
ip ipFlag ip ipFlag
) )
hostNameF := flags.VarPF( hostNameF := ctx.flags.VarPF(
&hostName, &hostName,
"hostname", "n", "hostname", "n",
"Name of the host to generate bootstrap.json for", "Name of the host to generate bootstrap.json for",
) )
flags.VarP(&ip, "ip", "i", "IP of the new host. An available IP will be chosen if none is given.") ctx.flags.VarP(&ip, "ip", "i", "IP of the new host. An available IP will be chosen if none is given.")
canCreateHosts := flags.Bool( canCreateHosts := ctx.flags.Bool(
"can-create-hosts", "can-create-hosts",
false, false,
"The new host should have the ability to create hosts too", "The new host should have the ability to create hosts too",
) )
if err := flags.Parse(ctx.args); err != nil { ctx, err := ctx.withParsedFlags()
if err != nil {
return fmt.Errorf("parsing flags: %w", err) return fmt.Errorf("parsing flags: %w", err)
} }
@ -43,11 +43,7 @@ var subCmdHostCreate = subCmd{
return errors.New("--hostname is required") return errors.New("--hostname is required")
} }
var ( res, err := newDaemonRPCClient().CreateHost(
res network.JoiningBootstrap
err error
)
res, err = ctx.daemonRPC.CreateHost(
ctx, hostName.V, network.CreateHostOpts{ ctx, hostName.V, network.CreateHostOpts{
IP: ip.V, IP: ip.V,
CanCreateHosts: *canCreateHosts, CanCreateHosts: *canCreateHosts,
@ -65,6 +61,11 @@ var subCmdHostList = subCmd{
name: "list", name: "list",
descr: "Lists all hosts in the network, and their IPs", descr: "Lists all hosts in the network, and their IPs",
do: func(ctx subCmdCtx) error { do: func(ctx subCmdCtx) error {
ctx, err := ctx.withParsedFlags()
if err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
hostsRes, err := ctx.getHosts() hostsRes, err := ctx.getHosts()
if err != nil { if err != nil {
return fmt.Errorf("calling GetHosts: %w", err) return fmt.Errorf("calling GetHosts: %w", err)
@ -102,17 +103,17 @@ var subCmdHostRemove = subCmd{
descr: "Removes a host from the network", descr: "Removes a host from the network",
do: func(ctx subCmdCtx) error { do: func(ctx subCmdCtx) error {
var ( var (
flags = ctx.flagSet(false)
hostName hostNameFlag hostName hostNameFlag
) )
hostNameF := flags.VarPF( hostNameF := ctx.flags.VarPF(
&hostName, &hostName,
"hostname", "n", "hostname", "n",
"Name of the host to remove", "Name of the host to remove",
) )
if err := flags.Parse(ctx.args); err != nil { ctx, err := ctx.withParsedFlags()
if err != nil {
return fmt.Errorf("parsing flags: %w", err) return fmt.Errorf("parsing flags: %w", err)
} }
@ -120,7 +121,7 @@ var subCmdHostRemove = subCmd{
return errors.New("--hostname is required") return errors.New("--hostname is required")
} }
if err := ctx.daemonRPC.RemoveHost(ctx, hostName.V); err != nil { if err := newDaemonRPCClient().RemoveHost(ctx, hostName.V); err != nil {
return fmt.Errorf("calling RemoveHost: %w", err) return fmt.Errorf("calling RemoveHost: %w", err)
} }

View File

@ -56,8 +56,8 @@ func main() {
err := subCmdCtx{ err := subCmdCtx{
Context: ctx, Context: ctx,
args: os.Args[1:],
logger: logger, logger: logger,
args: os.Args[1:],
}.doSubCmd( }.doSubCmd(
subCmdDaemon, subCmdDaemon,
subCmdGarage, subCmdGarage,

View File

@ -12,23 +12,21 @@ var subCmdNebulaCreateCert = subCmd{
name: "create-cert", name: "create-cert",
descr: "Creates a signed nebula certificate file for an existing host and writes it to stdout", descr: "Creates a signed nebula certificate file for an existing host and writes it to stdout",
do: func(ctx subCmdCtx) error { do: func(ctx subCmdCtx) error {
var (
flags = ctx.flagSet(false)
hostName hostNameFlag
)
hostNameF := flags.VarPF( var hostName hostNameFlag
hostNameF := ctx.flags.VarPF(
&hostName, &hostName,
"hostname", "n", "hostname", "n",
"Name of the host to generate a certificate for", "Name of the host to generate a certificate for",
) )
pubKeyPath := flags.StringP( pubKeyPath := ctx.flags.StringP(
"public-key-path", "p", "", "public-key-path", "p", "",
`Path to PEM file containing public key which will be embedded in the cert.`, `Path to PEM file containing public key which will be embedded in the cert.`,
) )
if err := flags.Parse(ctx.args); err != nil { ctx, err := ctx.withParsedFlags()
if err != nil {
return fmt.Errorf("parsing flags: %w", err) return fmt.Errorf("parsing flags: %w", err)
} }
@ -46,7 +44,7 @@ var subCmdNebulaCreateCert = subCmd{
return fmt.Errorf("unmarshaling public key as PEM: %w", err) return fmt.Errorf("unmarshaling public key as PEM: %w", err)
} }
res, err := ctx.daemonRPC.CreateNebulaCertificate( res, err := newDaemonRPCClient().CreateNebulaCertificate(
ctx, hostName.V, hostPub, ctx, hostName.V, hostPub,
) )
if err != nil { if err != nil {
@ -70,9 +68,8 @@ var subCmdNebulaShow = subCmd{
name: "show", name: "show",
descr: "Writes nebula network information to stdout in JSON format", descr: "Writes nebula network information to stdout in JSON format",
do: func(ctx subCmdCtx) error { do: func(ctx subCmdCtx) error {
ctx, err := ctx.withParsedFlags()
flags := ctx.flagSet(false) if err != nil {
if err := flags.Parse(ctx.args); err != nil {
return fmt.Errorf("parsing flags: %w", err) return fmt.Errorf("parsing flags: %w", err)
} }
@ -81,7 +78,7 @@ var subCmdNebulaShow = subCmd{
return fmt.Errorf("getting hosts: %w", err) return fmt.Errorf("getting hosts: %w", err)
} }
caPublicCreds, err := ctx.daemonRPC.GetNebulaCAPublicCredentials(ctx) caPublicCreds, err := newDaemonRPCClient().GetNebulaCAPublicCredentials(ctx)
if err != nil { if err != nil {
return fmt.Errorf("calling GetNebulaCAPublicCredentials: %w", err) return fmt.Errorf("calling GetNebulaCAPublicCredentials: %w", err)
} }

View File

@ -5,42 +5,44 @@ import (
"fmt" "fmt"
"isle/daemon/network" "isle/daemon/network"
"isle/jsonutil" "isle/jsonutil"
"os"
) )
var subCmdNetworkCreate = subCmd{ var subCmdNetworkCreate = subCmd{
name: "create", name: "create",
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.",
noNetwork: true,
do: func(ctx subCmdCtx) error { do: func(ctx subCmdCtx) error {
var ( var (
flags = ctx.flagSet(false)
ipNet ipNetFlag ipNet ipNetFlag
hostName hostNameFlag hostName hostNameFlag
) )
name := flags.StringP( name := ctx.flags.StringP(
"name", "N", "", "name", "N", "",
"Human-readable name to identify the network as.", "Human-readable name to identify the network as.",
) )
domain := flags.StringP( domain := ctx.flags.StringP(
"domain", "d", "", "domain", "d", "",
"Domain name that should be used as the root domain in the network.", "Domain name that should be used as the root domain in the network.",
) )
ipNetF := flags.VarPF( ipNetF := ctx.flags.VarPF(
&ipNet, "ip-net", "i", &ipNet, "ip-net", "i",
`An IP subnet, in CIDR form, which will be the overall range of`+ `An IP subnet, in CIDR form, which will be the overall range of`+
` possible IPs in the network. The first IP in this network`+ ` possible IPs in the network. The first IP in this network`+
` range will become this first host's IP.`, ` range will become this first host's IP.`,
) )
hostNameF := flags.VarPF( hostNameF := ctx.flags.VarPF(
&hostName, &hostName,
"hostname", "n", "hostname", "n",
"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",
) )
if err := flags.Parse(ctx.args); err != nil { ctx, err := ctx.withParsedFlags()
if err != nil {
return fmt.Errorf("parsing flags: %w", err) return fmt.Errorf("parsing flags: %w", err)
} }
@ -51,7 +53,7 @@ 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")
} }
err := ctx.daemonRPC.CreateNetwork( err = newDaemonRPCClient().CreateNetwork(
ctx, *name, *domain, ipNet.V, hostName.V, ctx, *name, *domain, ipNet.V, hostName.V,
) )
if err != nil { if err != nil {
@ -65,15 +67,14 @@ var subCmdNetworkCreate = subCmd{
var subCmdNetworkJoin = subCmd{ var subCmdNetworkJoin = subCmd{
name: "join", name: "join",
descr: "Joins this host to an existing network", descr: "Joins this host to an existing network",
noNetwork: true,
do: func(ctx subCmdCtx) error { do: func(ctx subCmdCtx) error {
var ( bootstrapPath := ctx.flags.StringP(
flags = ctx.flagSet(false)
bootstrapPath = flags.StringP(
"bootstrap-path", "b", "", "Path to a bootstrap.json file.", "bootstrap-path", "b", "", "Path to a bootstrap.json file.",
) )
)
if err := flags.Parse(ctx.args); err != nil { ctx, err := ctx.withParsedFlags()
if err != nil {
return fmt.Errorf("parsing flags: %w", err) return fmt.Errorf("parsing flags: %w", err)
} }
@ -88,17 +89,38 @@ var subCmdNetworkJoin = subCmd{
) )
} }
return ctx.daemonRPC.JoinNetwork(ctx, newBootstrap) return newDaemonRPCClient().JoinNetwork(ctx, newBootstrap)
},
}
var subCmdNetworkList = subCmd{
name: "list",
descr: "Lists all networks which have been joined",
noNetwork: true,
do: func(ctx subCmdCtx) error {
ctx, err := ctx.withParsedFlags()
if err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
creationParams, err := newDaemonRPCClient().GetNetworks(ctx)
if err != nil {
return fmt.Errorf("getting joined networks: %w", err)
}
return jsonutil.WriteIndented(os.Stdout, creationParams)
}, },
} }
var subCmdNetwork = subCmd{ var subCmdNetwork = subCmd{
name: "network", name: "network",
descr: "Sub-commands related to network membership", descr: "Sub-commands related to network membership",
plural: "s",
do: func(ctx subCmdCtx) error { do: func(ctx subCmdCtx) error {
return ctx.doSubCmd( return ctx.doSubCmd(
subCmdNetworkCreate, subCmdNetworkCreate,
subCmdNetworkJoin, subCmdNetworkJoin,
subCmdNetworkList,
) )
}, },
} }

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"fmt" "fmt"
"isle/daemon" "isle/daemon"
"isle/daemon/jsonrpc2"
"os" "os"
"strings" "strings"
@ -14,30 +13,8 @@ import (
type flagSet struct { type flagSet struct {
*pflag.FlagSet *pflag.FlagSet
}
func (fs flagSet) Parse(args []string) error { network string
fs.VisitAll(func(f *pflag.Flag) {
if f.Shorthand == "h" {
panic(fmt.Sprintf("flag %+v has reserved shorthand `-h`", f))
}
if f.Name == "help" {
panic(fmt.Sprintf("flag %+v has reserved name `--help`", f))
}
})
return fs.FlagSet.Parse(args)
}
// subCmdCtx contains all information available to a subCmd's do method.
type subCmdCtx struct {
context.Context
subCmd subCmd // the subCmd itself
args []string // command-line arguments, excluding the subCmd itself.
subCmdNames []string // names of subCmds so far, including this one
logger *mlog.Logger
daemonRPC daemon.RPC
} }
type subCmd struct { type subCmd struct {
@ -47,11 +24,74 @@ type subCmd struct {
// If set then the name will be allowed to be suffixed with this string. // If set then the name will be allowed to be suffixed with this string.
plural string plural string
// noNetwork, if true, means the call doesn't require a network to be
// specified on the command-line if there are more than one networks
// configured.
noNetwork bool
// Extra arguments on the command-line will be passed through to some
// underlying command.
passthroughArgs bool
} }
func (ctx subCmdCtx) usagePrefix() string { // subCmdCtx contains all information available to a subCmd's do method.
type subCmdCtx struct {
context.Context
logger *mlog.Logger
subCmdNamesStr := strings.Join(ctx.subCmdNames, " ") subCmd subCmd // the subCmd itself
args []string // command-line arguments, excluding the subCmd itself.
subCmdNames []string // names of subCmds so far, including this one
flags flagSet
}
func newSubCmdCtx(
ctx context.Context,
logger *mlog.Logger,
subCmd subCmd,
args []string,
subCmdNames []string,
) subCmdCtx {
flags := pflag.NewFlagSet(subCmd.name, pflag.ExitOnError)
flags.Usage = func() {
var passthroughStr string
if subCmd.passthroughArgs {
passthroughStr = " [--] [args...]"
}
fmt.Fprintf(
os.Stderr, "%s[-h|--help] [%s flags...]%s\n\n",
usagePrefix(subCmdNames), subCmd.name, passthroughStr,
)
fmt.Fprintf(os.Stderr, "%s FLAGS:\n\n", strings.ToUpper(subCmd.name))
fmt.Fprintln(os.Stderr, flags.FlagUsages())
os.Stderr.Sync()
os.Exit(2)
}
fs := flagSet{FlagSet: flags}
if !subCmd.noNetwork {
fs.FlagSet.StringVar(
&fs.network, "network", "", "Which network to perform the command against, if more than one is joined. Can be ID, name, or domain",
)
}
return subCmdCtx{
Context: ctx,
logger: logger,
subCmd: subCmd,
args: args,
subCmdNames: subCmdNames,
flags: fs,
}
}
func usagePrefix(subCmdNames []string) string {
subCmdNamesStr := strings.Join(subCmdNames, " ")
if subCmdNamesStr != "" { if subCmdNamesStr != "" {
subCmdNamesStr += " " subCmdNamesStr += " "
} }
@ -59,26 +99,22 @@ func (ctx subCmdCtx) usagePrefix() string {
return fmt.Sprintf("\nUSAGE: %s %s", os.Args[0], subCmdNamesStr) return fmt.Sprintf("\nUSAGE: %s %s", os.Args[0], subCmdNamesStr)
} }
func (ctx subCmdCtx) flagSet(withPassthrough bool) flagSet { func (ctx subCmdCtx) withParsedFlags() (subCmdCtx, error) {
flags := pflag.NewFlagSet(ctx.subCmd.name, pflag.ExitOnError) ctx.flags.VisitAll(func(f *pflag.Flag) {
flags.Usage = func() { if f.Shorthand == "h" {
panic(fmt.Sprintf("flag %+v has reserved shorthand `-h`", f))
}
if f.Name == "help" {
panic(fmt.Sprintf("flag %+v has reserved name `--help`", f))
}
})
var passthroughStr string if err := ctx.flags.Parse(ctx.args); err != nil {
if withPassthrough { return ctx, err
passthroughStr = " [--] [args...]"
} }
fmt.Fprintf( ctx.Context = daemon.WithNetwork(ctx.Context, ctx.flags.network)
os.Stderr, "%s[-h|--help] [%s flags...]%s\n\n", return ctx, nil
ctx.usagePrefix(), ctx.subCmd.name, passthroughStr,
)
fmt.Fprintf(os.Stderr, "%s FLAGS:\n\n", strings.ToUpper(ctx.subCmd.name))
fmt.Fprintln(os.Stderr, flags.FlagUsages())
os.Stderr.Sync()
os.Exit(2)
}
return flagSet{flags}
} }
func (ctx subCmdCtx) doSubCmd(subCmds ...subCmd) error { func (ctx subCmdCtx) doSubCmd(subCmds ...subCmd) error {
@ -90,7 +126,7 @@ func (ctx subCmdCtx) doSubCmd(subCmds ...subCmd) error {
fmt.Fprintf( fmt.Fprintf(
os.Stderr, os.Stderr,
"%s<subCmd> [-h|--help] [sub-command flags...]\n", "%s<subCmd> [-h|--help] [sub-command flags...]\n",
ctx.usagePrefix(), usagePrefix(ctx.subCmdNames),
) )
fmt.Fprintf(os.Stderr, "\nSUB-COMMANDS:\n\n") fmt.Fprintf(os.Stderr, "\nSUB-COMMANDS:\n\n")
@ -123,28 +159,21 @@ func (ctx subCmdCtx) doSubCmd(subCmds ...subCmd) error {
} }
subCmdName, args := args[0], args[1:] subCmdName, args := args[0], args[1:]
subCmd, ok := subCmdsMap[subCmdName]
subCmd, ok := subCmdsMap[subCmdName]
if !ok { if !ok {
printUsageExit(subCmdName) printUsageExit(subCmdName)
} }
daemonRPC := daemon.RPCFromClient( nextSubCmdCtx := newSubCmdCtx(
jsonrpc2.NewUnixHTTPClient( ctx.Context,
daemon.HTTPSocketPath(), daemonHTTPRPCPath, ctx.logger,
), subCmd,
args,
append(ctx.subCmdNames, subCmdName),
) )
err := subCmd.do(subCmdCtx{ if err := subCmd.do(nextSubCmdCtx); err != nil {
Context: ctx.Context,
subCmd: subCmd,
args: args,
subCmdNames: append(ctx.subCmdNames, subCmdName),
logger: ctx.logger,
daemonRPC: daemonRPC,
})
if err != nil {
return err return err
} }

View File

@ -9,6 +9,7 @@ import (
var subCmdVersion = subCmd{ var subCmdVersion = subCmd{
name: "version", name: "version",
descr: "Dumps version and build info to stdout", descr: "Dumps version and build info to stdout",
noNetwork: true,
do: func(ctx subCmdCtx) error { do: func(ctx subCmdCtx) error {
versionPath := filepath.Join(envAppDirPath, "share/version") versionPath := filepath.Join(envAppDirPath, "share/version")

View File

@ -49,7 +49,7 @@ func (o *Opts) withDefaults() *Opts {
// - 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
daemonConfig daecommon.Config networkConfig daecommon.NetworkConfig
runtimeDir toolkit.Dir runtimeDir toolkit.Dir
opts Opts opts Opts
@ -63,7 +63,7 @@ func New(
logger *mlog.Logger, logger *mlog.Logger,
binDirPath string, binDirPath string,
secretsStore secrets.Store, secretsStore secrets.Store,
daemonConfig daecommon.Config, networkConfig daecommon.NetworkConfig,
runtimeDir toolkit.Dir, runtimeDir toolkit.Dir,
garageAdminToken string, garageAdminToken string,
hostBootstrap bootstrap.Bootstrap, hostBootstrap bootstrap.Bootstrap,
@ -81,7 +81,7 @@ func New(
c := &Children{ c := &Children{
logger: logger, logger: logger,
daemonConfig: daemonConfig, networkConfig: networkConfig,
runtimeDir: runtimeDir, runtimeDir: runtimeDir,
opts: *opts, opts: *opts,
} }
@ -90,7 +90,7 @@ func New(
ctx, ctx,
garageRPCSecret, garageRPCSecret,
binDirPath, binDirPath,
daemonConfig, networkConfig,
garageAdminToken, garageAdminToken,
hostBootstrap, hostBootstrap,
) )
@ -101,7 +101,7 @@ func New(
c.pmux = pmuxlib.NewPmux(pmuxConfig, c.opts.Stdout, c.opts.Stderr) c.pmux = pmuxlib.NewPmux(pmuxConfig, c.opts.Stdout, c.opts.Stderr)
initErr := c.postPmuxInit( initErr := c.postPmuxInit(
ctx, daemonConfig, garageAdminToken, hostBootstrap, ctx, networkConfig, garageAdminToken, hostBootstrap,
) )
if initErr != nil { if initErr != nil {
logger.Warn(ctx, "failed to initialize Children, shutting down child processes", err) logger.Warn(ctx, "failed to initialize Children, shutting down child processes", err)
@ -118,7 +118,7 @@ func New(
// successfully. // successfully.
func (c *Children) RestartDNSMasq(hostBootstrap bootstrap.Bootstrap) error { func (c *Children) RestartDNSMasq(hostBootstrap bootstrap.Bootstrap) error {
_, err := dnsmasqWriteConfig( _, err := dnsmasqWriteConfig(
c.runtimeDir.Path, c.daemonConfig, hostBootstrap, c.runtimeDir.Path, c.networkConfig, hostBootstrap,
) )
if err != nil { if err != nil {
return fmt.Errorf("writing new dnsmasq config: %w", err) return fmt.Errorf("writing new dnsmasq config: %w", err)
@ -134,7 +134,7 @@ func (c *Children) RestartDNSMasq(hostBootstrap bootstrap.Bootstrap) error {
// successfully. // successfully.
func (c *Children) RestartNebula(hostBootstrap bootstrap.Bootstrap) error { func (c *Children) RestartNebula(hostBootstrap bootstrap.Bootstrap) error {
_, err := nebulaWriteConfig( _, err := nebulaWriteConfig(
c.runtimeDir.Path, c.daemonConfig, hostBootstrap, c.runtimeDir.Path, c.networkConfig, hostBootstrap,
) )
if err != nil { if err != nil {
return fmt.Errorf("writing a new nebula config: %w", err) return fmt.Errorf("writing a new nebula config: %w", err)

View File

@ -18,14 +18,14 @@ type ReloadDiff struct {
// CalculateReloadDiff calculates a ReloadDiff based on an old and new // CalculateReloadDiff calculates a ReloadDiff based on an old and new
// bootstrap. // bootstrap.
func CalculateReloadDiff( func CalculateReloadDiff(
daemonConfig daecommon.Config, networkConfig daecommon.NetworkConfig,
prevBootstrap, nextBootstrap bootstrap.Bootstrap, prevBootstrap, nextBootstrap bootstrap.Bootstrap,
) ( ) (
diff ReloadDiff, err error, diff ReloadDiff, err error,
) { ) {
{ {
prevNebulaConfig, prevErr := nebulaConfig(daemonConfig, prevBootstrap) prevNebulaConfig, prevErr := nebulaConfig(networkConfig, prevBootstrap)
nextNebulaConfig, nextErr := nebulaConfig(daemonConfig, nextBootstrap) nextNebulaConfig, nextErr := nebulaConfig(networkConfig, nextBootstrap)
if err = errors.Join(prevErr, nextErr); err != nil { if err = errors.Join(prevErr, nextErr); err != nil {
err = fmt.Errorf("calculating nebula config: %w", err) err = fmt.Errorf("calculating nebula config: %w", err)
return return
@ -38,8 +38,8 @@ func CalculateReloadDiff(
{ {
diff.DNSChanged = !reflect.DeepEqual( diff.DNSChanged = !reflect.DeepEqual(
dnsmasqConfig(daemonConfig, prevBootstrap), dnsmasqConfig(networkConfig, prevBootstrap),
dnsmasqConfig(daemonConfig, nextBootstrap), dnsmasqConfig(networkConfig, nextBootstrap),
) )
} }

View File

@ -14,7 +14,7 @@ import (
) )
func dnsmasqConfig( func dnsmasqConfig(
daemonConfig daecommon.Config, hostBootstrap bootstrap.Bootstrap, networkConfig daecommon.NetworkConfig, hostBootstrap bootstrap.Bootstrap,
) dnsmasq.ConfData { ) dnsmasq.ConfData {
hostsSlice := make([]dnsmasq.ConfDataHost, 0, len(hostBootstrap.Hosts)) hostsSlice := make([]dnsmasq.ConfDataHost, 0, len(hostBootstrap.Hosts))
for _, host := range hostBootstrap.Hosts { for _, host := range hostBootstrap.Hosts {
@ -29,7 +29,7 @@ func dnsmasqConfig(
}) })
return dnsmasq.ConfData{ return dnsmasq.ConfData{
Resolvers: daemonConfig.DNS.Resolvers, Resolvers: networkConfig.DNS.Resolvers,
Domain: hostBootstrap.NetworkCreationParams.Domain, Domain: hostBootstrap.NetworkCreationParams.Domain,
IP: hostBootstrap.ThisHost().IP().String(), IP: hostBootstrap.ThisHost().IP().String(),
Hosts: hostsSlice, Hosts: hostsSlice,
@ -38,14 +38,14 @@ func dnsmasqConfig(
func dnsmasqWriteConfig( func dnsmasqWriteConfig(
runtimeDirPath string, runtimeDirPath string,
daemonConfig daecommon.Config, networkConfig daecommon.NetworkConfig,
hostBootstrap bootstrap.Bootstrap, hostBootstrap bootstrap.Bootstrap,
) ( ) (
string, error, string, error,
) { ) {
var ( var (
confPath = filepath.Join(runtimeDirPath, "dnsmasq.conf") confPath = filepath.Join(runtimeDirPath, "dnsmasq.conf")
confData = dnsmasqConfig(daemonConfig, hostBootstrap) confData = dnsmasqConfig(networkConfig, hostBootstrap)
) )
if err := dnsmasq.WriteConfFile(confPath, confData); err != nil { if err := dnsmasq.WriteConfFile(confPath, confData); err != nil {
@ -58,13 +58,13 @@ func dnsmasqWriteConfig(
func dnsmasqPmuxProcConfig( func dnsmasqPmuxProcConfig(
logger *mlog.Logger, logger *mlog.Logger,
runtimeDirPath, binDirPath string, runtimeDirPath, binDirPath string,
daemonConfig daecommon.Config, networkConfig daecommon.NetworkConfig,
hostBootstrap bootstrap.Bootstrap, hostBootstrap bootstrap.Bootstrap,
) ( ) (
pmuxlib.ProcessConfig, error, pmuxlib.ProcessConfig, error,
) { ) {
confPath, err := dnsmasqWriteConfig( confPath, err := dnsmasqWriteConfig(
runtimeDirPath, daemonConfig, hostBootstrap, runtimeDirPath, networkConfig, hostBootstrap,
) )
if err != nil { if err != nil {
return pmuxlib.ProcessConfig{}, fmt.Errorf( return pmuxlib.ProcessConfig{}, fmt.Errorf(

View File

@ -23,12 +23,12 @@ func garageAdminClientLogger(logger *mlog.Logger) *mlog.Logger {
func waitForGarage( func waitForGarage(
ctx context.Context, ctx context.Context,
logger *mlog.Logger, logger *mlog.Logger,
daemonConfig daecommon.Config, networkConfig daecommon.NetworkConfig,
adminToken string, adminToken string,
hostBootstrap bootstrap.Bootstrap, hostBootstrap bootstrap.Bootstrap,
) error { ) error {
allocs := daemonConfig.Storage.Allocations allocs := networkConfig.Storage.Allocations
// if this host doesn't have any allocations specified then fall back to // if this host doesn't have any allocations specified then fall back to
// waiting for nebula // waiting for nebula
@ -108,7 +108,7 @@ func garagePmuxProcConfigs(
ctx context.Context, ctx context.Context,
logger *mlog.Logger, logger *mlog.Logger,
rpcSecret, runtimeDirPath, binDirPath string, rpcSecret, runtimeDirPath, binDirPath string,
daemonConfig daecommon.Config, networkConfig daecommon.NetworkConfig,
adminToken string, adminToken string,
hostBootstrap bootstrap.Bootstrap, hostBootstrap bootstrap.Bootstrap,
) ( ) (
@ -116,7 +116,7 @@ func garagePmuxProcConfigs(
) { ) {
var ( var (
pmuxProcConfigs = map[string]pmuxlib.ProcessConfig{} pmuxProcConfigs = map[string]pmuxlib.ProcessConfig{}
allocs = daemonConfig.Storage.Allocations allocs = networkConfig.Storage.Allocations
) )
if len(allocs) > 0 && rpcSecret == "" { if len(allocs) > 0 && rpcSecret == "" {

View File

@ -48,7 +48,7 @@ func waitForNebula(
} }
func nebulaConfig( func nebulaConfig(
daemonConfig daecommon.Config, networkConfig daecommon.NetworkConfig,
hostBootstrap bootstrap.Bootstrap, hostBootstrap bootstrap.Bootstrap,
) ( ) (
map[string]any, error, map[string]any, error,
@ -95,12 +95,12 @@ func nebulaConfig(
"respond": true, "respond": true,
}, },
"tun": map[string]any{ "tun": map[string]any{
"dev": daemonConfig.VPN.Tun.Device, "dev": networkConfig.VPN.Tun.Device,
}, },
"firewall": daemonConfig.VPN.Firewall, "firewall": networkConfig.VPN.Firewall,
} }
if publicAddr := daemonConfig.VPN.PublicAddr; publicAddr == "" { if publicAddr := networkConfig.VPN.PublicAddr; publicAddr == "" {
config["listen"] = map[string]string{ config["listen"] = map[string]string{
"host": "0.0.0.0", "host": "0.0.0.0",
@ -137,12 +137,12 @@ func nebulaConfig(
func nebulaWriteConfig( func nebulaWriteConfig(
runtimeDirPath string, runtimeDirPath string,
daemonConfig daecommon.Config, networkConfig daecommon.NetworkConfig,
hostBootstrap bootstrap.Bootstrap, hostBootstrap bootstrap.Bootstrap,
) ( ) (
string, error, string, error,
) { ) {
config, err := nebulaConfig(daemonConfig, hostBootstrap) config, err := nebulaConfig(networkConfig, hostBootstrap)
if err != nil { if err != nil {
return "", fmt.Errorf("creating nebula config: %w", err) return "", fmt.Errorf("creating nebula config: %w", err)
} }
@ -158,12 +158,12 @@ func nebulaWriteConfig(
func nebulaPmuxProcConfig( func nebulaPmuxProcConfig(
runtimeDirPath, binDirPath string, runtimeDirPath, binDirPath string,
daemonConfig daecommon.Config, networkConfig daecommon.NetworkConfig,
hostBootstrap bootstrap.Bootstrap, hostBootstrap bootstrap.Bootstrap,
) ( ) (
pmuxlib.ProcessConfig, error, pmuxlib.ProcessConfig, error,
) { ) {
config, err := nebulaConfig(daemonConfig, hostBootstrap) config, err := nebulaConfig(networkConfig, hostBootstrap)
if err != nil { if err != nil {
return pmuxlib.ProcessConfig{}, fmt.Errorf( return pmuxlib.ProcessConfig{}, fmt.Errorf(
"creating nebula config: %w", err, "creating nebula config: %w", err,

View File

@ -12,7 +12,7 @@ import (
func (c *Children) newPmuxConfig( func (c *Children) newPmuxConfig(
ctx context.Context, ctx context.Context,
garageRPCSecret, binDirPath string, garageRPCSecret, binDirPath string,
daemonConfig daecommon.Config, networkConfig daecommon.NetworkConfig,
garageAdminToken string, garageAdminToken string,
hostBootstrap bootstrap.Bootstrap, hostBootstrap bootstrap.Bootstrap,
) ( ) (
@ -21,7 +21,7 @@ func (c *Children) newPmuxConfig(
nebulaPmuxProcConfig, err := nebulaPmuxProcConfig( nebulaPmuxProcConfig, err := nebulaPmuxProcConfig(
c.runtimeDir.Path, c.runtimeDir.Path,
binDirPath, binDirPath,
daemonConfig, networkConfig,
hostBootstrap, hostBootstrap,
) )
if err != nil { if err != nil {
@ -32,7 +32,7 @@ func (c *Children) newPmuxConfig(
c.logger, c.logger,
c.runtimeDir.Path, c.runtimeDir.Path,
binDirPath, binDirPath,
daemonConfig, networkConfig,
hostBootstrap, hostBootstrap,
) )
if err != nil { if err != nil {
@ -47,7 +47,7 @@ func (c *Children) newPmuxConfig(
garageRPCSecret, garageRPCSecret,
c.runtimeDir.Path, c.runtimeDir.Path,
binDirPath, binDirPath,
daemonConfig, networkConfig,
garageAdminToken, garageAdminToken,
hostBootstrap, hostBootstrap,
) )
@ -68,7 +68,7 @@ func (c *Children) newPmuxConfig(
func (c *Children) postPmuxInit( func (c *Children) postPmuxInit(
ctx context.Context, ctx context.Context,
daemonConfig daecommon.Config, networkConfig daecommon.NetworkConfig,
garageAdminToken string, garageAdminToken string,
hostBootstrap bootstrap.Bootstrap, hostBootstrap bootstrap.Bootstrap,
) error { ) error {
@ -79,7 +79,7 @@ func (c *Children) postPmuxInit(
c.logger.Info(ctx, "Waiting for garage instances to come online") c.logger.Info(ctx, "Waiting for garage instances to come online")
err := waitForGarage( err := waitForGarage(
ctx, c.logger, daemonConfig, garageAdminToken, hostBootstrap, ctx, c.logger, networkConfig, garageAdminToken, hostBootstrap,
) )
if err != nil { if err != nil {
return fmt.Errorf("waiting for garage to start: %w", err) return fmt.Errorf("waiting for garage to start: %w", err)

View File

@ -86,6 +86,15 @@ func (c *rpcClient) GetNebulaCAPublicCredentials(ctx context.Context) (c2 nebula
return return
} }
func (c *rpcClient) GetNetworks(ctx context.Context) (ca1 []bootstrap.CreationParams, err error) {
err = c.client.Call(
ctx,
&ca1,
"GetNetworks",
)
return
}
func (c *rpcClient) JoinNetwork(ctx context.Context, j1 network.JoiningBootstrap) (err error) { func (c *rpcClient) JoinNetwork(ctx context.Context, j1 network.JoiningBootstrap) (err error) {
err = c.client.Call( err = c.client.Call(
ctx, ctx,

View File

@ -4,6 +4,8 @@ import (
"errors" "errors"
"fmt" "fmt"
"io/fs" "io/fs"
"isle/bootstrap"
"isle/daemon/daecommon"
"os" "os"
"path/filepath" "path/filepath"
"slices" "slices"
@ -40,6 +42,26 @@ var HTTPSocketPath = sync.OnceValue(func() string {
) )
}) })
func pickNetworkConfig(
daemonConfig daecommon.Config, creationParams bootstrap.CreationParams,
) (
daecommon.NetworkConfig, bool,
) {
if len(daemonConfig.Networks) == 1 { // DEPRECATED
if c, ok := daemonConfig.Networks[daecommon.DeprecatedNetworkID]; ok {
return c, true
}
}
for searchStr, networkConfig := range daemonConfig.Networks {
if creationParams.Matches(searchStr) {
return networkConfig, true
}
}
return daecommon.NetworkConfig{}, false
}
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
// Jigs // Jigs

20
go/daemon/ctx.go Normal file
View File

@ -0,0 +1,20 @@
package daemon
import (
"context"
"isle/daemon/jsonrpc2"
)
const metaKeyNetworkSearchStr = "daemon.networkSearchStr"
// WithNetwork returns the Context so that, when used against a daemon RPC
// endpoint, the endpoint knows which network is being targetted for the call.
// The network can be identified by its ID, name, or domain.
func WithNetwork(ctx context.Context, searchStr string) context.Context {
return jsonrpc2.WithMeta(ctx, metaKeyNetworkSearchStr, searchStr)
}
func getNetworkSearchStr(ctx context.Context) string {
v, _ := jsonrpc2.GetMeta(ctx)[metaKeyNetworkSearchStr].(string)
return v
}

View File

@ -4,15 +4,21 @@ import (
"fmt" "fmt"
"io" "io"
"isle/bootstrap" "isle/bootstrap"
"isle/yamlutil" "isle/toolkit"
"net"
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
"github.com/imdario/mergo"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
const (
// Network ID used when translating from the old single-network daemon
// config to the multi-network config.
DeprecatedNetworkID = "_" // DEPRECATED
)
func defaultConfigPath(appDirPath string) string { func defaultConfigPath(appDirPath string) string {
return filepath.Join(appDirPath, "etc", "daemon.yml") return filepath.Join(appDirPath, "etc", "daemon.yml")
} }
@ -22,18 +28,10 @@ type ConfigTun struct {
} }
type ConfigFirewall struct { type ConfigFirewall struct {
Conntrack ConfigConntrack `yaml:"conntrack"`
Outbound []ConfigFirewallRule `yaml:"outbound"` Outbound []ConfigFirewallRule `yaml:"outbound"`
Inbound []ConfigFirewallRule `yaml:"inbound"` Inbound []ConfigFirewallRule `yaml:"inbound"`
} }
type ConfigConntrack struct {
TCPTimeout string `yaml:"tcp_timeout"`
UDPTimeout string `yaml:"udp_timeout"`
DefaultTimeout string `yaml:"default_timeout"`
MaxConnections int `yaml:"max_connections"`
}
type ConfigFirewallRule struct { type ConfigFirewallRule struct {
Port string `yaml:"port,omitempty"` Port string `yaml:"port,omitempty"`
Code string `yaml:"code,omitempty"` Code string `yaml:"code,omitempty"`
@ -61,8 +59,8 @@ type ConfigStorageAllocation struct {
Zone string `yaml:"zone"` Zone string `yaml:"zone"`
} }
// Config describes the structure of the daemon config file. // NetworkConfig describes the configuration of a single network.
type Config struct { type NetworkConfig struct {
DNS struct { DNS struct {
Resolvers []string `yaml:"resolvers"` Resolvers []string `yaml:"resolvers"`
} `yaml:"dns"` } `yaml:"dns"`
@ -76,7 +74,37 @@ type Config struct {
} `yaml:"storage"` } `yaml:"storage"`
} }
func (c *Config) fillDefaults() { func (c *NetworkConfig) fillDefaults() {
if c.DNS.Resolvers == nil {
c.DNS.Resolvers = []string{
"1.1.1.1",
"8.8.8.8",
}
}
if c.VPN.Firewall.Outbound == nil {
c.VPN.Firewall.Outbound = []ConfigFirewallRule{
{
Port: "any",
Proto: "any",
Host: "any",
},
}
}
if c.VPN.Firewall.Inbound == nil {
c.VPN.Firewall.Inbound = []ConfigFirewallRule{
{
Port: "any",
Proto: "icmp",
Host: "any",
},
}
}
if c.VPN.Tun.Device == "" {
c.VPN.Tun.Device = "isle-tun"
}
var firewallGarageInbound []ConfigFirewallRule var firewallGarageInbound []ConfigFirewallRule
@ -116,6 +144,40 @@ func (c *Config) fillDefaults() {
) )
} }
// Config describes the structure of the daemon config file.
type Config struct {
Networks map[string]NetworkConfig `yaml:"networks"`
}
// Validate asserts that the Config has no internal inconsistencies which would
// render it unusable.
func (c Config) Validate() error {
nebulaPorts := map[string]string{}
for id, network := range c.Networks {
if network.VPN.PublicAddr == "" {
continue
}
_, port, err := net.SplitHostPort(network.VPN.PublicAddr)
if err != nil {
return fmt.Errorf(
"invalid vpn.public_addr %q: %w", network.VPN.PublicAddr, err,
)
} else if otherID, ok := nebulaPorts[port]; ok {
return fmt.Errorf(
"two networks with the same vpn.public_addr: %q and %q",
id,
otherID,
)
}
nebulaPorts[port] = id
}
return nil
}
// CopyDefaultConfig copies the daemon config file embedded in the AppDir into // CopyDefaultConfig copies the daemon config file embedded in the AppDir into
// the given io.Writer. // the given io.Writer.
func CopyDefaultConfig(into io.Writer, appDirPath string) error { func CopyDefaultConfig(into io.Writer, appDirPath string) error {
@ -136,51 +198,45 @@ func CopyDefaultConfig(into io.Writer, appDirPath string) error {
return nil return nil
} }
// LoadConfig loads the daemon config from userConfigPath, merges it with // LoadConfig loads the daemon config from userConfigPath.
// the default found in the appDirPath, and returns the result.
// //
// If userConfigPath is not given then the default is loaded and returned. // If userConfigPath is not given then the default is loaded and returned.
func LoadConfig( func LoadConfig(userConfigPath string) (Config, error) {
appDirPath, userConfigPath string, if userConfigPath == "" {
) ( return Config{}, nil
Config, error,
) {
defaultConfigPath := defaultConfigPath(appDirPath)
var fullDaemon map[string]interface{}
if err := yamlutil.LoadYamlFile(&fullDaemon, defaultConfigPath); err != nil {
return Config{}, fmt.Errorf("parsing default daemon config file: %w", err)
} }
if userConfigPath != "" { userConfigB, err := os.ReadFile(userConfigPath)
var daemonConfig map[string]interface{}
if err := yamlutil.LoadYamlFile(&daemonConfig, userConfigPath); err != nil {
return Config{}, fmt.Errorf("parsing %q: %w", userConfigPath, err)
}
err := mergo.Merge(&fullDaemon, daemonConfig, mergo.WithOverride)
if err != nil { if err != nil {
return Config{}, fmt.Errorf("merging contents of file %q: %w", userConfigPath, err) return Config{}, fmt.Errorf("reading from file: %w", err)
}
} }
fullDaemonB, err := yaml.Marshal(fullDaemon) { // DEPRECATED
var networkConfig NetworkConfig
if err != nil { _ = yaml.Unmarshal(userConfigB, &networkConfig)
return Config{}, fmt.Errorf("yaml marshaling: %w", err) if !toolkit.IsZero(networkConfig) {
networkConfig.fillDefaults()
config := Config{
Networks: map[string]NetworkConfig{
DeprecatedNetworkID: networkConfig,
},
}
return config, config.Validate()
}
} }
var config Config var config Config
if err := yaml.Unmarshal(fullDaemonB, &config); err != nil { if err := yaml.Unmarshal(userConfigB, &config); err != nil {
return Config{}, fmt.Errorf("yaml unmarshaling back into Config struct: %w", err) return Config{}, fmt.Errorf("yaml unmarshaling back into Config struct: %w", err)
} }
config.fillDefaults() for id := range config.Networks {
network := config.Networks[id]
network.fillDefaults()
config.Networks[id] = network
}
return config, nil return config, config.Validate()
} }
// BootstrapGarageHostForAlloc returns the bootstrap.GarageHostInstance which // BootstrapGarageHostForAlloc returns the bootstrap.GarageHostInstance which

View File

@ -4,6 +4,7 @@ package daemon
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"isle/bootstrap" "isle/bootstrap"
"isle/daemon/children" "isle/daemon/children"
@ -11,6 +12,7 @@ import (
"isle/daemon/network" "isle/daemon/network"
"isle/nebula" "isle/nebula"
"isle/toolkit" "isle/toolkit"
"sort"
"sync" "sync"
"dev.mediocregopher.com/mediocre-go-lib.git/mctx" "dev.mediocregopher.com/mediocre-go-lib.git/mctx"
@ -65,7 +67,7 @@ type Daemon struct {
networksRuntimeDir toolkit.Dir networksRuntimeDir toolkit.Dir
l sync.RWMutex l sync.RWMutex
network network.Network networks map[string]network.Network
} }
// New initializes and returns a Daemon. // New initializes and returns a Daemon.
@ -78,11 +80,24 @@ func New(
) ( ) (
*Daemon, error, *Daemon, error,
) { ) {
opts = opts.withDefaults()
if err := migrateToMultiNetworkStateDirectory(
ctx,
logger.WithNamespace("migration-multi-network-state-dir"),
opts.EnvVars,
); err != nil {
return nil, fmt.Errorf(
"migrating to multi-network state directory: %w", err,
)
}
d := &Daemon{ d := &Daemon{
logger: logger, logger: logger,
daemonConfig: daemonConfig, daemonConfig: daemonConfig,
envBinDirPath: envBinDirPath, envBinDirPath: envBinDirPath,
opts: opts.withDefaults(), opts: opts,
networks: map[string]network.Network{},
} }
{ {
@ -100,18 +115,14 @@ func New(
} }
} }
loadableNetworks, err := LoadableNetworks(d.networksStateDir) loadableNetworks, err := loadableNetworks(d.networksStateDir)
if err != nil { if err != nil {
return nil, fmt.Errorf("listing loadable networks: %w", err) return nil, fmt.Errorf("listing loadable networks: %w", err)
} }
if len(loadableNetworks) > 1 { for _, creationParams := range loadableNetworks {
return nil, fmt.Errorf( id := creationParams.ID
"more then one loadable Network found: %+v", loadableNetworks, ctx = mctx.WithAnnotator(ctx, creationParams)
)
} else if len(loadableNetworks) == 1 {
id := loadableNetworks[0].ID
ctx = mctx.WithAnnotator(ctx, loadableNetworks[0])
networkStateDir, networkRuntimeDir, err := networkDirs( networkStateDir, networkRuntimeDir, err := networkDirs(
d.networksStateDir, d.networksRuntimeDir, id, true, d.networksStateDir, d.networksRuntimeDir, id, true,
@ -122,11 +133,13 @@ func New(
) )
} }
d.network, err = network.Load( networkConfig, _ := pickNetworkConfig(daemonConfig, creationParams)
d.networks[id], err = network.Load(
ctx, ctx,
logger.WithNamespace("network"), logger.WithNamespace("network"),
id, id,
d.daemonConfig, networkConfig,
d.envBinDirPath, d.envBinDirPath,
networkStateDir, networkStateDir,
networkRuntimeDir, networkRuntimeDir,
@ -162,10 +175,19 @@ func (d *Daemon) CreateNetwork(
creationParams := bootstrap.NewCreationParams(name, domain) creationParams := bootstrap.NewCreationParams(name, domain)
ctx = mctx.WithAnnotator(ctx, creationParams) ctx = mctx.WithAnnotator(ctx, creationParams)
networkConfig, ok := pickNetworkConfig(
d.daemonConfig, creationParams,
)
if !ok {
return errors.New("couldn't find network config for network being created")
}
d.l.Lock() d.l.Lock()
defer d.l.Unlock() defer d.l.Unlock()
if d.network != nil { if joined, err := alreadyJoined(ctx, d.networks, creationParams); err != nil {
return fmt.Errorf("checking if already joined to network: %w", err)
} else if joined {
return ErrAlreadyJoined return ErrAlreadyJoined
} }
@ -184,7 +206,7 @@ func (d *Daemon) CreateNetwork(
n, err := network.Create( n, err := network.Create(
ctx, ctx,
d.logger.WithNamespace("network"), d.logger.WithNamespace("network"),
d.daemonConfig, networkConfig,
d.envBinDirPath, d.envBinDirPath,
networkStateDir, networkStateDir,
networkRuntimeDir, networkRuntimeDir,
@ -200,7 +222,7 @@ func (d *Daemon) CreateNetwork(
} }
d.logger.Info(ctx, "Network created successfully") d.logger.Info(ctx, "Network created successfully")
d.network = n d.networks[creationParams.ID] = n
return nil return nil
} }
@ -212,13 +234,20 @@ func (d *Daemon) CreateNetwork(
func (d *Daemon) JoinNetwork( func (d *Daemon) JoinNetwork(
ctx context.Context, newBootstrap network.JoiningBootstrap, ctx context.Context, newBootstrap network.JoiningBootstrap,
) error { ) error {
networkID := newBootstrap.Bootstrap.NetworkCreationParams.ID var (
creationParams = newBootstrap.Bootstrap.NetworkCreationParams
networkConfig, _ = pickNetworkConfig(d.daemonConfig, creationParams)
networkID = creationParams.ID
)
ctx = mctx.WithAnnotator(ctx, newBootstrap.Bootstrap.NetworkCreationParams) ctx = mctx.WithAnnotator(ctx, newBootstrap.Bootstrap.NetworkCreationParams)
d.l.Lock() d.l.Lock()
defer d.l.Unlock() defer d.l.Unlock()
if d.network != nil { if joined, err := alreadyJoined(ctx, d.networks, creationParams); err != nil {
return fmt.Errorf("checking if already joined to network: %w", err)
} else if joined {
return ErrAlreadyJoined return ErrAlreadyJoined
} }
@ -235,7 +264,7 @@ func (d *Daemon) JoinNetwork(
n, err := network.Join( n, err := network.Join(
ctx, ctx,
d.logger.WithNamespace("network"), d.logger.WithNamespace("network"),
d.daemonConfig, networkConfig,
newBootstrap, newBootstrap,
d.envBinDirPath, d.envBinDirPath,
networkStateDir, networkStateDir,
@ -251,7 +280,7 @@ func (d *Daemon) JoinNetwork(
} }
d.logger.Info(ctx, "Network joined successfully") d.logger.Info(ctx, "Network joined successfully")
d.network = n d.networks[networkID] = n
return nil return nil
} }
@ -265,12 +294,42 @@ func withNetwork[Res any](
d.l.RLock() d.l.RLock()
defer d.l.RUnlock() defer d.l.RUnlock()
if d.network == nil { network, err := pickNetwork(ctx, d.networks, d.networksStateDir)
if err != nil {
var zero Res var zero Res
return zero, ErrNoNetwork return zero, nil
} }
return fn(ctx, d.network) return fn(ctx, network)
}
// GetNetworks returns all networks which have been joined by the Daemon,
// ordered by their name.
func (d *Daemon) GetNetworks(
ctx context.Context,
) (
[]bootstrap.CreationParams, error,
) {
d.l.RLock()
defer d.l.RUnlock()
res := make([]bootstrap.CreationParams, 0, len(d.networks))
for id, network := range d.networks {
creationParams, err := network.GetNetworkCreationParams(ctx)
if err != nil {
return nil, fmt.Errorf(
"getting network creation params of network %q: %w", id, err,
)
}
res = append(res, creationParams)
}
sort.Slice(res, func(i, j int) bool {
return res[i].Name < res[j].Name
})
return res, nil
} }
// GetHost implements the method for the network.RPC interface. // GetHost implements the method for the network.RPC interface.
@ -390,32 +449,28 @@ func (d *Daemon) Shutdown() error {
d.l.Lock() d.l.Lock()
defer d.l.Unlock() defer d.l.Unlock()
if d.network != nil { var (
return d.network.Shutdown() errCh = make(chan error, len(d.networks))
errs []error
)
for id := range d.networks {
var (
id = id
n = d.networks[id]
)
go func() {
if err := n.Shutdown(); err != nil {
errCh <- fmt.Errorf("shutting down network %q: %w", id, err)
}
errCh <- nil
}()
} }
return nil
//var ( for range cap(errCh) {
// errCh = make(chan error, len(d.networks)) errs = append(errs, <-errCh)
// errs []error }
//)
//for id := range d.networks { return errors.Join(errs...)
// id := id
// n := d.networks[id]
// go func() {
// if err := n.Shutdown(); err != nil {
// errCh <- fmt.Errorf("shutting down network %q: %w", id, err)
// }
// errCh <- nil
// }()
//}
//for range cap(errCh) {
// if err := <-errCh; err != nil {
// errs = append(errs, err)
// }
//}
//return errors.Join(errs...)
} }

View File

@ -8,6 +8,8 @@ import (
const ( const (
errCodeNoNetwork = daecommon.ErrorCodeRangeDaemon + iota errCodeNoNetwork = daecommon.ErrorCodeRangeDaemon + iota
errCodeAlreadyJoined errCodeAlreadyJoined
errCodeNoMatchingNetworks
errCodeMultipleMatchingNetworks
) )
var ( var (
@ -16,6 +18,19 @@ var (
ErrNoNetwork = jsonrpc2.NewError(errCodeNoNetwork, "No network configured") ErrNoNetwork = jsonrpc2.NewError(errCodeNoNetwork, "No network configured")
// ErrAlreadyJoined is returned when the daemon is instructed to create or // ErrAlreadyJoined is returned when the daemon is instructed to create or
// join a new network, but it is already joined to a network. // join a new network, but it is already joined to that network.
ErrAlreadyJoined = jsonrpc2.NewError(errCodeAlreadyJoined, "Already joined to a network") ErrAlreadyJoined = jsonrpc2.NewError(errCodeAlreadyJoined, "Already joined to a network")
// ErrNoMatchingNetworks is returned if the search string didn't match any
// networks.
ErrNoMatchingNetworks = jsonrpc2.NewError(
errCodeNoMatchingNetworks, "No networks matched the search string",
)
// ErrMultipleMatchingNetworks is returned if the search string matched
// multiple networks.
ErrMultipleMatchingNetworks = jsonrpc2.NewError(
errCodeMultipleMatchingNetworks,
"Multiple networks matched the search string",
)
) )

View File

@ -13,7 +13,11 @@ type Client interface {
// receiver pointer, unless it is nil in which case the result will be // receiver pointer, unless it is nil in which case the result will be
// discarded. // discarded.
// //
// If the Context was produced using WithMeta then that metadata will be
// carried with the request to the server via the Meta field of the
// RequestParams.
//
// If an error result is returned from the server that will be returned as // If an error result is returned from the server that will be returned as
// an Error struct. // an Error struct.
Call(ctx context.Context, rcv any, method string, params ...any) error Call(ctx context.Context, rcv any, method string, args ...any) error
} }

View File

@ -49,14 +49,14 @@ func NewUnixHTTPClient(unixSocketPath, reqPath string) Client {
} }
func (c *httpClient) Call( func (c *httpClient) Call(
ctx context.Context, rcv any, method string, params ...any, ctx context.Context, rcv any, method string, args ...any,
) error { ) error {
var ( var (
body = new(bytes.Buffer) body = new(bytes.Buffer)
enc = json.NewEncoder(body) enc = json.NewEncoder(body)
) )
id, err := encodeRequest(enc, method, params) id, err := encodeRequest(ctx, enc, method, args)
if err != nil { if err != nil {
return fmt.Errorf("encoding request: %w", err) return fmt.Errorf("encoding request: %w", err)
} }

View File

@ -19,9 +19,9 @@ func NewReadWriterClient(rw io.ReadWriter) Client {
} }
func (c rwClient) Call( func (c rwClient) Call(
ctx context.Context, rcv any, method string, params ...any, ctx context.Context, rcv any, method string, args ...any,
) error { ) error {
id, err := encodeRequest(c.enc, method, params) id, err := encodeRequest(ctx, c.enc, method, args)
if err != nil { if err != nil {
return fmt.Errorf("encoding request: %w", err) return fmt.Errorf("encoding request: %w", err)
} }

View File

@ -18,19 +18,21 @@ type methodDispatchFunc func(context.Context, Request) (any, error)
func newMethodDispatchFunc( func newMethodDispatchFunc(
method reflect.Value, method reflect.Value,
) methodDispatchFunc { ) methodDispatchFunc {
paramTs := make([]reflect.Type, method.Type().NumIn()-1) argTs := make([]reflect.Type, method.Type().NumIn()-1)
for i := range paramTs { for i := range argTs {
paramTs[i] = method.Type().In(i + 1) argTs[i] = method.Type().In(i + 1)
} }
return func(ctx context.Context, req Request) (any, error) { return func(ctx context.Context, req Request) (any, error) {
callVals := make([]reflect.Value, 0, len(paramTs)+1) ctx = context.WithValue(ctx, ctxKeyMeta(0), req.Params.Meta)
callVals := make([]reflect.Value, 0, len(argTs)+1)
callVals = append(callVals, reflect.ValueOf(ctx)) callVals = append(callVals, reflect.ValueOf(ctx))
for i, paramT := range paramTs { for i, argT := range argTs {
paramPtrV := reflect.New(paramT) argPtrV := reflect.New(argT)
err := json.Unmarshal(req.Params[i], paramPtrV.Interface()) err := json.Unmarshal(req.Params.Args[i], argPtrV.Interface())
if err != nil { if err != nil {
// The JSON has already been validated, so this is not an // The JSON has already been validated, so this is not an
// errCodeParse situation. We assume it's an invalid param then, // errCodeParse situation. We assume it's an invalid param then,
@ -38,13 +40,13 @@ func newMethodDispatchFunc(
// returning an Error of its own. // returning an Error of its own.
if !errors.As(err, new(Error)) { if !errors.As(err, new(Error)) {
err = NewInvalidParamsError( err = NewInvalidParamsError(
"JSON unmarshaling param %d into %T: %v", i, paramT, err, "JSON unmarshaling arg %d into %T: %v", i, argT, err,
) )
} }
return nil, err return nil, err
} }
callVals = append(callVals, paramPtrV.Elem()) callVals = append(callVals, argPtrV.Elem())
} }
var ( var (
@ -82,7 +84,8 @@ type dispatcher struct {
// MethodName(context.Context, ...ParamType) (ResponseType, error) // MethodName(context.Context, ...ParamType) (ResponseType, error)
// MethodName(context.Context, ...ParamType) error // MethodName(context.Context, ...ParamType) error
// //
// will be available via RPC calls. // will be available via RPC calls. Any Meta data in the request can be obtained
// within the method handler by calling GetMeta on the method's Context.
func NewDispatchHandler(i any) Handler { func NewDispatchHandler(i any) Handler {
v := reflect.ValueOf(i) v := reflect.ValueOf(i)
if v.Kind() != reflect.Pointer { if v.Kind() != reflect.Pointer {

View File

@ -57,15 +57,16 @@ func NewMLogMiddleware(logger *mlog.Logger) Middleware {
ctx, ctx,
"rpcRequestID", req.ID, "rpcRequestID", req.ID,
"rpcRequestMethod", req.Method, "rpcRequestMethod", req.Method,
"rpcRequestMeta", req.Params.Meta,
) )
if logger.MaxLevel() >= mlog.LevelDebug.Int() { if logger.MaxLevel() >= mlog.LevelDebug.Int() {
ctx := ctx ctx := ctx
for i := range req.Params { for i := range req.Params.Args {
ctx = mctx.Annotate( ctx = mctx.Annotate(
ctx, ctx,
fmt.Sprintf("rpcRequestParam%d", i), fmt.Sprintf("rpcRequestArgs%d", i),
string(req.Params[i]), string(req.Params.Args[i]),
) )
} }
logger.Debug(ctx, "Handling RPC request") logger.Debug(ctx, "Handling RPC request")

View File

@ -3,6 +3,7 @@
package jsonrpc2 package jsonrpc2
import ( import (
"context"
"crypto/rand" "crypto/rand"
"encoding/json" "encoding/json"
"fmt" "fmt"
@ -11,11 +12,19 @@ import (
const version = "2.0" const version = "2.0"
// RequestParams are the parameters passed in a Request. Meta contains
// information that is not directly related to what is being requested, while
// Args are the request's actual arguments.
type RequestParams struct {
Meta map[string]any `json:"meta,omitempty"`
Args []json.RawMessage `json:"args,omitempty"`
}
// Request encodes an RPC request according to the spec. // Request encodes an RPC request according to the spec.
type Request struct { type Request struct {
Version string `json:"jsonrpc"` // must be "2.0" Version string `json:"jsonrpc"` // must be "2.0"
Method string `json:"method"` Method string `json:"method"`
Params []json.RawMessage `json:"params,omitempty"` Params RequestParams `json:"params,omitempty"`
ID string `json:"id"` ID string `json:"id"`
} }
@ -37,18 +46,18 @@ func newID() string {
// encodeRequest writes a request to an io.Writer, returning the ID of the // encodeRequest writes a request to an io.Writer, returning the ID of the
// request. // request.
func encodeRequest( func encodeRequest(
enc *json.Encoder, method string, params []any, ctx context.Context, enc *json.Encoder, method string, args []any,
) ( ) (
string, error, string, error,
) { ) {
var ( var (
paramsBs = make([]json.RawMessage, len(params)) argsBs = make([]json.RawMessage, len(args))
err error err error
) )
for i := range params { for i := range args {
paramsBs[i], err = json.Marshal(params[i]) argsBs[i], err = json.Marshal(args[i])
if err != nil { if err != nil {
return "", fmt.Errorf("encoding param %d as JSON: %w", i, err) return "", fmt.Errorf("encoding arg %d as JSON: %w", i, err)
} }
} }
@ -57,7 +66,10 @@ func encodeRequest(
reqEnvelope = Request{ reqEnvelope = Request{
Version: version, Version: version,
Method: method, Method: method,
Params: paramsBs, Params: RequestParams{
Meta: GetMeta(ctx),
Args: argsBs,
},
ID: id, ID: id,
} }
) )

View File

@ -48,6 +48,16 @@ func (i dividerImpl) Noop(ctx context.Context) error {
return nil return nil
} }
func (i dividerImpl) Divide2FromMeta(ctx context.Context) (int, error) {
var (
meta = GetMeta(ctx)
top = int(meta["top"].(float64))
bottom = int(meta["bottom"].(float64))
)
return i.Divide2(ctx, top, bottom)
}
func (dividerImpl) Hidden(ctx context.Context, p struct{}) (int, error) { func (dividerImpl) Hidden(ctx context.Context, p struct{}) (int, error) {
return 0, errors.New("Shouldn't be possible to call this!") return 0, errors.New("Shouldn't be possible to call this!")
} }
@ -57,6 +67,7 @@ type divider interface {
Divide(ctx context.Context, p DivideParams) (int, error) Divide(ctx context.Context, p DivideParams) (int, error)
One(ctx context.Context) (int, error) One(ctx context.Context) (int, error)
Noop(ctx context.Context) error Noop(ctx context.Context) error
Divide2FromMeta(ctx context.Context) (int, error)
} }
var testHandler = func() Handler { var testHandler = func() Handler {
@ -124,6 +135,19 @@ func testClient(t *testing.T, client Client) {
} }
}) })
t.Run("success/meta", func(t *testing.T) {
ctx = WithMeta(ctx, "top", 6)
ctx = WithMeta(ctx, "bottom", 2)
var res int
err := client.Call(ctx, &res, "Divide2FromMeta")
if err != nil {
t.Fatal(err)
} else if res != 3 {
t.Fatalf("expected 2, got %d", res)
}
})
t.Run("err/application", func(t *testing.T) { t.Run("err/application", func(t *testing.T) {
err := client.Call(ctx, nil, "Divide", DivideParams{}) err := client.Call(ctx, nil, "Divide", DivideParams{})
if !errors.Is(err, ErrDivideByZero) { if !errors.Is(err, ErrDivideByZero) {

View File

@ -0,0 +1,30 @@
package jsonrpc2
import (
"context"
"maps"
)
type ctxKeyMeta int
// WithMeta returns a Context where the given key will be set to the given value
// in the Meta field of all JSONRPC2 requests made using Clients from this
// package.
func WithMeta(ctx context.Context, key string, value any) context.Context {
m, _ := ctx.Value(ctxKeyMeta(0)).(map[string]any)
if m == nil {
m = map[string]any{}
} else {
m = maps.Clone(m)
}
m[key] = value
return context.WithValue(ctx, ctxKeyMeta(0), m)
}
// GetMeta returns all key/values which have been set on the Context using
// WithMeta. This may return nil if WithMeta was never called.
func GetMeta(ctx context.Context) map[string]any {
m, _ := ctx.Value(ctxKeyMeta(0)).(map[string]any)
return m
}

86
go/daemon/migrations.go Normal file
View File

@ -0,0 +1,86 @@
package daemon
import (
"context"
"errors"
"fmt"
"io/fs"
"isle/daemon/daecommon"
"isle/jsonutil"
"os"
"path/filepath"
"dev.mediocregopher.com/mediocre-go-lib.git/mctx"
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
)
// DEPRECATED
func migrateToMultiNetworkStateDirectory(
ctx context.Context, logger *mlog.Logger, envVars daecommon.EnvVars,
) error {
var (
legacyBootstrapPath = filepath.Join(
envVars.StateDir.Path, "bootstrap.json",
)
legacySecretsPath = filepath.Join(envVars.StateDir.Path, "secrets")
)
if _, err := os.Stat(legacyBootstrapPath); errors.Is(err, fs.ErrNotExist) {
return nil // no bootstrap in the legacy path
} else if err != nil {
return fmt.Errorf("checking file %q: %w", legacyBootstrapPath, err)
}
var bootstrapBody struct {
NetworkCreationParams struct {
ID string
}
}
if err := jsonutil.LoadFile(
&bootstrapBody, legacyBootstrapPath,
); err != nil {
return fmt.Errorf(
"loading bootstrap from %q: %w", legacyBootstrapPath, err,
)
}
var (
networkStateDirPath = filepath.Join(
envVars.StateDir.Path,
"networks",
bootstrapBody.NetworkCreationParams.ID,
)
newBootstrapPath = filepath.Join(networkStateDirPath, "bootstrap.json")
newSecretsPath = filepath.Join(networkStateDirPath, "secrets")
)
ctx = mctx.Annotate(
ctx,
"legacyBootstrapPath", legacyBootstrapPath,
"legacySecretsPath", legacySecretsPath,
"newBootstrapPath", newBootstrapPath,
"newSecretsPath", newSecretsPath,
)
logger.Info(ctx, "Migrating to multi-network state directory layout")
if err := os.MkdirAll(networkStateDirPath, 0700); err != nil {
return fmt.Errorf("creating %q: %w", networkStateDirPath, err)
}
if err := os.Rename(legacyBootstrapPath, newBootstrapPath); err != nil {
return fmt.Errorf(
"renaming %q to %q: %w", legacyBootstrapPath, newBootstrapPath, err,
)
}
if err := os.Rename(legacySecretsPath, newSecretsPath); err != nil {
return fmt.Errorf(
"renaming %q to %q: %w", legacySecretsPath, newSecretsPath, err,
)
}
return nil
}

View File

@ -1,6 +1,7 @@
package daemon package daemon
import ( import (
"context"
"fmt" "fmt"
"isle/bootstrap" "isle/bootstrap"
"isle/daemon/network" "isle/daemon/network"
@ -41,9 +42,9 @@ func networkDirs(
return return
} }
// LoadableNetworks returns the CreationParams for each Network which is able to // loadableNetworks returns the CreationParams for each Network which is able to
// be loaded. // be loaded.
func LoadableNetworks( func loadableNetworks(
networksStateDir toolkit.Dir, networksStateDir toolkit.Dir,
) ( ) (
[]bootstrap.CreationParams, error, []bootstrap.CreationParams, error,
@ -71,3 +72,60 @@ func LoadableNetworks(
return creationParams, nil return creationParams, nil
} }
func pickNetwork(
ctx context.Context,
networks map[string]network.Network,
networksStateDir toolkit.Dir,
) (
network.Network, error,
) {
if len(networks) == 0 {
return nil, ErrNoNetwork
}
creationParams, err := loadableNetworks(networksStateDir)
if err != nil {
return nil, fmt.Errorf("getting loadable networks: %w", err)
}
var (
networkSearchStr = getNetworkSearchStr(ctx)
matchingNetworkIDs = make([]string, 0, len(networks))
)
for _, creationParam := range creationParams {
if networkSearchStr == "" || creationParam.Matches(networkSearchStr) {
matchingNetworkIDs = append(matchingNetworkIDs, creationParam.ID)
}
}
if len(matchingNetworkIDs) == 0 {
return nil, ErrNoMatchingNetworks
} else if len(matchingNetworkIDs) > 1 {
return nil, ErrMultipleMatchingNetworks
}
return networks[matchingNetworkIDs[0]], nil
}
func alreadyJoined(
ctx context.Context,
networks map[string]network.Network,
creationParams bootstrap.CreationParams,
) (
bool, error,
) {
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
}

View File

@ -29,8 +29,8 @@ func writeBootstrapToStateDir(
return nil return nil
} }
func coalesceDaemonConfigAndBootstrap( func coalesceNetworkConfigAndBootstrap(
daemonConfig daecommon.Config, hostBootstrap bootstrap.Bootstrap, networkConfig daecommon.NetworkConfig, hostBootstrap bootstrap.Bootstrap,
) ( ) (
bootstrap.Bootstrap, error, bootstrap.Bootstrap, error,
) { ) {
@ -38,12 +38,12 @@ func coalesceDaemonConfigAndBootstrap(
HostAssigned: hostBootstrap.HostAssigned, HostAssigned: hostBootstrap.HostAssigned,
HostConfigured: bootstrap.HostConfigured{ HostConfigured: bootstrap.HostConfigured{
Nebula: bootstrap.NebulaHost{ Nebula: bootstrap.NebulaHost{
PublicAddr: daemonConfig.VPN.PublicAddr, PublicAddr: networkConfig.VPN.PublicAddr,
}, },
}, },
} }
if allocs := daemonConfig.Storage.Allocations; len(allocs) > 0 { if allocs := networkConfig.Storage.Allocations; len(allocs) > 0 {
for i, alloc := range allocs { for i, alloc := range allocs {

View File

@ -57,7 +57,7 @@ func garageAdminClientLogger(logger *mlog.Logger) *mlog.Logger {
// or it will _panic_ if there is no local instance configured. // or it will _panic_ if there is no local instance configured.
func newGarageAdminClient( func newGarageAdminClient(
logger *mlog.Logger, logger *mlog.Logger,
daemonConfig daecommon.Config, networkConfig daecommon.NetworkConfig,
adminToken string, adminToken string,
hostBootstrap bootstrap.Bootstrap, hostBootstrap bootstrap.Bootstrap,
) *garage.AdminClient { ) *garage.AdminClient {
@ -68,7 +68,7 @@ func newGarageAdminClient(
garageAdminClientLogger(logger), garageAdminClientLogger(logger),
net.JoinHostPort( net.JoinHostPort(
thisHost.IP().String(), thisHost.IP().String(),
strconv.Itoa(daemonConfig.Storage.Allocations[0].AdminPort), strconv.Itoa(networkConfig.Storage.Allocations[0].AdminPort),
), ),
adminToken, adminToken,
) )
@ -77,18 +77,18 @@ func newGarageAdminClient(
func garageApplyLayout( func garageApplyLayout(
ctx context.Context, ctx context.Context,
logger *mlog.Logger, logger *mlog.Logger,
daemonConfig daecommon.Config, networkConfig daecommon.NetworkConfig,
adminToken string, adminToken string,
hostBootstrap bootstrap.Bootstrap, hostBootstrap bootstrap.Bootstrap,
) error { ) error {
var ( var (
adminClient = newGarageAdminClient( adminClient = newGarageAdminClient(
logger, daemonConfig, adminToken, hostBootstrap, logger, networkConfig, adminToken, hostBootstrap,
) )
thisHost = hostBootstrap.ThisHost() thisHost = hostBootstrap.ThisHost()
hostName = thisHost.Name hostName = thisHost.Name
allocs = daemonConfig.Storage.Allocations allocs = networkConfig.Storage.Allocations
peers = make([]garage.PeerLayout, len(allocs)) peers = make([]garage.PeerLayout, len(allocs))
) )
@ -115,14 +115,14 @@ func garageApplyLayout(
func garageInitializeGlobalBucket( func garageInitializeGlobalBucket(
ctx context.Context, ctx context.Context,
logger *mlog.Logger, logger *mlog.Logger,
daemonConfig daecommon.Config, networkConfig daecommon.NetworkConfig,
adminToken string, adminToken string,
hostBootstrap bootstrap.Bootstrap, hostBootstrap bootstrap.Bootstrap,
) ( ) (
garage.S3APICredentials, error, garage.S3APICredentials, error,
) { ) {
adminClient := newGarageAdminClient( adminClient := newGarageAdminClient(
logger, daemonConfig, adminToken, hostBootstrap, logger, networkConfig, adminToken, hostBootstrap,
) )
creds, err := adminClient.CreateS3APICredentials( creds, err := adminClient.CreateS3APICredentials(

View File

@ -1,14 +0,0 @@
package network
import (
"crypto/rand"
"encoding/hex"
)
func randStr(l int) string {
b := make([]byte, l)
if _, err := rand.Read(b); err != nil {
panic(err)
}
return hex.EncodeToString(b)
}

View File

@ -18,7 +18,6 @@ import (
"isle/nebula" "isle/nebula"
"isle/secrets" "isle/secrets"
"isle/toolkit" "isle/toolkit"
"log"
"net/netip" "net/netip"
"slices" "slices"
"sync" "sync"
@ -154,7 +153,7 @@ func (o *Opts) withDefaults() *Opts {
type network struct { type network struct {
logger *mlog.Logger logger *mlog.Logger
daemonConfig daecommon.Config networkConfig daecommon.NetworkConfig
envBinDirPath string envBinDirPath string
stateDir toolkit.Dir stateDir toolkit.Dir
@ -178,21 +177,20 @@ type network struct {
func instatiateNetwork( func instatiateNetwork(
logger *mlog.Logger, logger *mlog.Logger,
networkID string, networkID string,
daemonConfig daecommon.Config, networkConfig daecommon.NetworkConfig,
envBinDirPath string, envBinDirPath string,
stateDir toolkit.Dir, stateDir toolkit.Dir,
runtimeDir toolkit.Dir, runtimeDir toolkit.Dir,
opts *Opts, opts *Opts,
) *network { ) *network {
log.Printf("DEBUG: network stateDir:%+v runtimeDir:%+v", stateDir, runtimeDir)
return &network{ return &network{
logger: logger, logger: logger,
daemonConfig: daemonConfig, networkConfig: networkConfig,
envBinDirPath: envBinDirPath, envBinDirPath: envBinDirPath,
stateDir: stateDir, stateDir: stateDir,
runtimeDir: runtimeDir, runtimeDir: runtimeDir,
opts: opts.withDefaults(), opts: opts.withDefaults(),
garageAdminToken: randStr(32), garageAdminToken: toolkit.RandStr(32),
shutdownCh: make(chan struct{}), shutdownCh: make(chan struct{}),
} }
} }
@ -227,7 +225,7 @@ func Load(
ctx context.Context, ctx context.Context,
logger *mlog.Logger, logger *mlog.Logger,
networkID string, networkID string,
daemonConfig daecommon.Config, networkConfig daecommon.NetworkConfig,
envBinDirPath string, envBinDirPath string,
stateDir toolkit.Dir, stateDir toolkit.Dir,
runtimeDir toolkit.Dir, runtimeDir toolkit.Dir,
@ -238,7 +236,7 @@ func Load(
n := instatiateNetwork( n := instatiateNetwork(
logger, logger,
networkID, networkID,
daemonConfig, networkConfig,
envBinDirPath, envBinDirPath,
stateDir, stateDir,
runtimeDir, runtimeDir,
@ -272,7 +270,7 @@ func Load(
func Join( func Join(
ctx context.Context, ctx context.Context,
logger *mlog.Logger, logger *mlog.Logger,
daemonConfig daecommon.Config, networkConfig daecommon.NetworkConfig,
joiningBootstrap JoiningBootstrap, joiningBootstrap JoiningBootstrap,
envBinDirPath string, envBinDirPath string,
stateDir toolkit.Dir, stateDir toolkit.Dir,
@ -284,7 +282,7 @@ func Join(
n := instatiateNetwork( n := instatiateNetwork(
logger, logger,
joiningBootstrap.Bootstrap.NetworkCreationParams.ID, joiningBootstrap.Bootstrap.NetworkCreationParams.ID,
daemonConfig, networkConfig,
envBinDirPath, envBinDirPath,
stateDir, stateDir,
runtimeDir, runtimeDir,
@ -324,7 +322,7 @@ func Join(
func Create( func Create(
ctx context.Context, ctx context.Context,
logger *mlog.Logger, logger *mlog.Logger,
daemonConfig daecommon.Config, networkConfig daecommon.NetworkConfig,
envBinDirPath string, envBinDirPath string,
stateDir toolkit.Dir, stateDir toolkit.Dir,
runtimeDir toolkit.Dir, runtimeDir toolkit.Dir,
@ -335,7 +333,7 @@ func Create(
) ( ) (
Network, error, Network, error,
) { ) {
if len(daemonConfig.Storage.Allocations) < 3 { if len(networkConfig.Storage.Allocations) < 3 {
return nil, ErrInvalidConfig.WithData( return nil, ErrInvalidConfig.WithData(
"At least three storage allocations are required.", "At least three storage allocations are required.",
) )
@ -346,12 +344,12 @@ func Create(
return nil, fmt.Errorf("creating nebula CA cert: %w", err) return nil, fmt.Errorf("creating nebula CA cert: %w", err)
} }
garageRPCSecret := randStr(32) garageRPCSecret := toolkit.RandStr(32)
n := instatiateNetwork( n := instatiateNetwork(
logger, logger,
creationParams.ID, creationParams.ID,
daemonConfig, networkConfig,
envBinDirPath, envBinDirPath,
stateDir, stateDir,
runtimeDir, runtimeDir,
@ -409,8 +407,8 @@ func (n *network) initialize(
// by the daemon config. This way the network has the most up-to-date // by the daemon config. This way the network has the most up-to-date
// possible bootstrap. This updated bootstrap will later get updated in // possible bootstrap. This updated bootstrap will later get updated in
// garage as a background task, so other hosts will see it as well. // garage as a background task, so other hosts will see it as well.
currBootstrap, err := coalesceDaemonConfigAndBootstrap( currBootstrap, err := coalesceNetworkConfigAndBootstrap(
n.daemonConfig, currBootstrap, n.networkConfig, currBootstrap,
) )
if err != nil { if err != nil {
return fmt.Errorf("combining configuration into bootstrap: %w", err) return fmt.Errorf("combining configuration into bootstrap: %w", err)
@ -429,7 +427,7 @@ func (n *network) initialize(
n.logger.WithNamespace("children"), n.logger.WithNamespace("children"),
n.envBinDirPath, n.envBinDirPath,
n.secretsStore, n.secretsStore,
n.daemonConfig, n.networkConfig,
n.runtimeDir, n.runtimeDir,
n.garageAdminToken, n.garageAdminToken,
currBootstrap, currBootstrap,
@ -467,10 +465,10 @@ func (n *network) initialize(
} }
func (n *network) postInit(ctx context.Context) error { func (n *network) postInit(ctx context.Context) error {
if len(n.daemonConfig.Storage.Allocations) > 0 { if len(n.networkConfig.Storage.Allocations) > 0 {
n.logger.Info(ctx, "Applying garage layout") n.logger.Info(ctx, "Applying garage layout")
if err := garageApplyLayout( if err := garageApplyLayout(
ctx, n.logger, n.daemonConfig, n.garageAdminToken, n.currBootstrap, ctx, n.logger, n.networkConfig, n.garageAdminToken, n.currBootstrap,
); err != nil { ); err != nil {
return fmt.Errorf("applying garage layout: %w", err) return fmt.Errorf("applying garage layout: %w", err)
} }
@ -489,7 +487,7 @@ func (n *network) postInit(ctx context.Context) error {
garageGlobalBucketCreds, err := garageInitializeGlobalBucket( garageGlobalBucketCreds, err := garageInitializeGlobalBucket(
ctx, ctx,
n.logger, n.logger,
n.daemonConfig, n.networkConfig,
n.garageAdminToken, n.garageAdminToken,
n.currBootstrap, n.currBootstrap,
) )
@ -568,7 +566,7 @@ func (n *network) reload(
newBootstrap.Hosts[thisHost.Name] = thisHost newBootstrap.Hosts[thisHost.Name] = thisHost
diff, err := children.CalculateReloadDiff( diff, err := children.CalculateReloadDiff(
n.daemonConfig, currBootstrap, newBootstrap, n.networkConfig, currBootstrap, newBootstrap,
) )
if err != nil { if err != nil {
return fmt.Errorf("calculating diff between bootstraps: %w", err) return fmt.Errorf("calculating diff between bootstraps: %w", err)

View File

@ -2,6 +2,7 @@ package daemon
import ( import (
"context" "context"
"isle/bootstrap"
"isle/daemon/jsonrpc2" "isle/daemon/jsonrpc2"
"isle/daemon/network" "isle/daemon/network"
"isle/nebula" "isle/nebula"
@ -22,9 +23,19 @@ type RPC interface {
JoinNetwork(context.Context, network.JoiningBootstrap) error JoinNetwork(context.Context, network.JoiningBootstrap) error
GetNetworks(context.Context) ([]bootstrap.CreationParams, error)
// All network.RPC methods are automatically implemented by Daemon using the // All network.RPC methods are automatically implemented by Daemon using the
// currently joined network. If no network is joined then any call to these // currently joined network. If no network is joined then any call to these
// methods will return ErrNoNetwork. // methods will return ErrNoNetwork.
//
// All calls to these methods must be accompanied with a context produced by
// WithNetwork, in order to choose the network. These methods may return
// these errors, in addition to those documented on the individual methods:
//
// Errors:
// - ErrNoMatchingNetworks
// - ErrMultipleMatchingNetworks
network.RPC network.RPC
} }

View File

@ -1,3 +1,12 @@
// Package toolkit contains useful utilities which are not specific to any // Package toolkit contains useful utilities which are not specific to any
// specific part of isle. // specific part of isle.
package toolkit package toolkit
import "reflect"
// IsZero returns true if the value is equal to its zero value according to
// reflect.DeepEqual.
func IsZero[T any](v T) bool {
var zero T
return reflect.DeepEqual(v, zero)
}

View File

@ -33,6 +33,8 @@ if [ ! -d "$XDG_RUNTIME_DIR/isle" ]; then
mkdir c mkdir c
cat >daemon.yml <<EOF cat >daemon.yml <<EOF
networks:
testing:
vpn: vpn:
public_addr: 127.0.0.1:60000 public_addr: 127.0.0.1:60000
tun: tun: