Compare commits
9 Commits
af69f1cfba
...
038a28bb02
Author | SHA1 | Date | |
---|---|---|---|
038a28bb02 | |||
06a345ecd1 | |||
6c185f6263 | |||
53ad8a91b4 | |||
5138ed7c6a | |||
4f6a89ced0 | |||
39e12f6ebd | |||
d31be8455b | |||
ca62a37692 |
@ -105,7 +105,4 @@ Documentation for devs:
|
||||
|
||||
Besides documentation, there are a few other pages which might be useful:
|
||||
|
||||
* [Roadmap][roadmap]
|
||||
* [Glossary](docs/glossary.md)
|
||||
|
||||
[roadmap]: docs/roadmap.md
|
||||
|
128
docs/roadmap.md
128
docs/roadmap.md
@ -1,128 +0,0 @@
|
||||
# Roadmap
|
||||
|
||||
The following are rough outlines of upcoming work on the roadmap, roughly in the
|
||||
order they will be implemented.
|
||||
|
||||
## Main quest
|
||||
|
||||
These items are listed more or less in the order they need to be completed, as
|
||||
they generally depend on the items previous to them.
|
||||
|
||||
### Windows Support + GUI
|
||||
|
||||
Support for Windows is a must. This requirement also includes a simple GUI,
|
||||
which would essentially act as a thin layer on top of `daemon.yml` to start
|
||||
with.
|
||||
|
||||
Depending on difficulty level, OSX support might be added at this stage as well.
|
||||
|
||||
### NATS
|
||||
|
||||
Garage is currently used to handle eventually-consistent persistent storage, but
|
||||
there is no mechanism for inter-host realtime communication as of yet. NATS
|
||||
would be a good candidate for this, as it uses a gossip protocol which does not
|
||||
require a central coordinator (I don't think), and is well supported.
|
||||
|
||||
### Integration of [Caddy](https://caddyserver.com/docs/)
|
||||
|
||||
Integration of Caddy's will require some plugins to be developed. We want Caddy
|
||||
to be able to store cert information in S3 (garage), so that all isle lighthouse
|
||||
nodes can potentially become gateways as well. Once done, it would be possible
|
||||
for lighthouses to forward public traffic to inner nodes.
|
||||
|
||||
It should also be possible for users within the network to take use lighthouse
|
||||
Caddy's to host their websites (and eventually gemini capsules) for them.
|
||||
|
||||
Most likely this integration will require NATS as well, to coordinate cache
|
||||
invalidation and cert refreshing.
|
||||
|
||||
### Invitation code bootstrapping
|
||||
|
||||
Once an HTTP gateway/load-balancer is set up it should be possible to do host
|
||||
bootstrapping using invite codes rather than manually giving new users bootstrap
|
||||
files. The bootstrap file would be stored, encrypted, in garage, with the invite
|
||||
code being able to both identify and decrypt it. To instantiate a host, the user
|
||||
only needs to input the network domain name and the invite code.
|
||||
|
||||
### FUSE Mount
|
||||
|
||||
KBFS style. Every user should be able to mount virtual directories to their host
|
||||
which correspond to various buckets in garage.
|
||||
|
||||
- "public": editable amongst all users on the host, shared publicly via HTTP
|
||||
gateway.
|
||||
|
||||
- "protected": editable amongst all users on the host, but not accessible
|
||||
outside the network.
|
||||
|
||||
- "private": only accessible to a particular user (client-side encrypted).
|
||||
|
||||
Whether it's necessary to support directories which are shared only between
|
||||
specific users remains to be seen. The identification of a single "user" between
|
||||
different hosts is also an unsolved problem.
|
||||
|
||||
## Side quests
|
||||
|
||||
These items aren't necessarily required by the main quest, and aren't dependent
|
||||
on any other items being completed. They are nice-to-haves that we do want to
|
||||
eventually complete, but aren't the main focus.
|
||||
|
||||
### Design System
|
||||
|
||||
It would be great to get some help from a designer or otherwise
|
||||
artistically-minded person to create some kind of design framework which could
|
||||
be used across publicly-facing frontends. Such a system would provide a simple
|
||||
but cohesive vision for how things should look, include:
|
||||
|
||||
- Color schemes
|
||||
- Fonts and text decoration in different situations
|
||||
- Some simple, reusable layout templates (splash page, documentation, form)
|
||||
- Basic components like tables, lists, media, etc..
|
||||
|
||||
### DHCP
|
||||
|
||||
Currently all hosts require a static IP to be reserved by the admin. Nebula may
|
||||
support DHCP already, but if it doesn't we should look into how this could be
|
||||
accomplished. Depending on how reliable DNS support is it may be possible to use
|
||||
DHCP for all non-lighthouse hosts, which would be excellent.
|
||||
|
||||
### IPv6 network ranges
|
||||
|
||||
It should theoretically be possible for the internal network IP range to be on
|
||||
IPv6 rather than IPv4. This may be a simple matter of just testing it to confirm
|
||||
it works.
|
||||
|
||||
### Proper Linux Packages
|
||||
|
||||
Rather than distributing raw binaries for Linux we should instead be
|
||||
distributing actual packages.
|
||||
|
||||
* deb files for debian/ubuntu
|
||||
* PKGBUILD for arch (done)
|
||||
* rpm for fedora?
|
||||
* flatpak?
|
||||
|
||||
This will allow for properly setting capabilities for the binary at install
|
||||
time, so that it can be run as non-root, and installing any necessary `.desktop`
|
||||
files so that it can be run as a GUI application.
|
||||
|
||||
### Mobile app
|
||||
|
||||
To start with a simple mobile app which provided connectivity to the network
|
||||
would be great. We are not able to use the existing nebula mobile app because it
|
||||
is not actually open-source, but we can at least use it as a reference to see
|
||||
how this can be accomplished.
|
||||
|
||||
### DNS/Firewall Configuration
|
||||
|
||||
Ideally Isle could detect the DNS/firewall subsystems being used on a per-OS
|
||||
basis and configure them as needed. This would be simplify necessary
|
||||
documentation and setup steps for operators.
|
||||
|
||||
### Plugins
|
||||
|
||||
It would not be difficult to spec out a plugin system using nix commands.
|
||||
Existing components could be rigged to use this plugin system, and we could then
|
||||
use the system to add future components which might prove useful. Once the
|
||||
project is public such a system would be much appreciated I think, as it would
|
||||
let other groups rig their binaries with all sorts of new functionality.
|
@ -47,6 +47,10 @@ type Bootstrap struct {
|
||||
}
|
||||
|
||||
// New initializes and returns a new Bootstrap file for a new host.
|
||||
//
|
||||
// TODO in the resulting bootstrap only include this host and hosts which are
|
||||
// necessary for connecting to nebula/garage. Remember to immediately re-poll
|
||||
// garage for the full hosts list during network joining.
|
||||
func New(
|
||||
caCreds nebula.CACredentials,
|
||||
adminCreationParams CreationParams,
|
||||
|
@ -6,8 +6,7 @@ import (
|
||||
)
|
||||
|
||||
func (ctx subCmdCtx) getHosts() (daemon.GetHostsResult, error) {
|
||||
var res daemon.GetHostsResult
|
||||
err := ctx.daemonRCPClient.Call(ctx.ctx, &res, "GetHosts", nil)
|
||||
res, err := ctx.daemonRPC.GetHosts(ctx)
|
||||
if err != nil {
|
||||
return daemon.GetHostsResult{}, fmt.Errorf("calling GetHosts: %w", err)
|
||||
}
|
||||
|
@ -10,12 +10,16 @@ import (
|
||||
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
|
||||
)
|
||||
|
||||
// TODO it would be good to have an `isle daemon config-check` kind of command,
|
||||
// which could be run prior to a systemd service restart to make sure we don't
|
||||
// restart the service into a configuration that will definitely fail.
|
||||
|
||||
var subCmdDaemon = subCmd{
|
||||
name: "daemon",
|
||||
descr: "Runs the isle daemon (Default if no sub-command given)",
|
||||
do: func(subCmdCtx subCmdCtx) error {
|
||||
do: func(ctx subCmdCtx) error {
|
||||
|
||||
flags := subCmdCtx.flagSet(false)
|
||||
flags := ctx.flagSet(false)
|
||||
|
||||
daemonConfigPath := flags.StringP(
|
||||
"config-path", "c", "",
|
||||
@ -32,12 +36,10 @@ var subCmdDaemon = subCmd{
|
||||
`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(subCmdCtx.args); err != nil {
|
||||
if err := flags.Parse(ctx.args); err != nil {
|
||||
return fmt.Errorf("parsing flags: %w", err)
|
||||
}
|
||||
|
||||
ctx := subCmdCtx.ctx
|
||||
|
||||
if *dumpConfig {
|
||||
return daemon.CopyDefaultConfig(os.Stdout, envAppDirPath)
|
||||
}
|
||||
@ -47,7 +49,7 @@ var subCmdDaemon = subCmd{
|
||||
return fmt.Errorf("couldn't parse log level %q", *logLevelStr)
|
||||
}
|
||||
|
||||
logger := subCmdCtx.logger.WithMaxLevel(logLevel.Int())
|
||||
logger := ctx.logger.WithMaxLevel(logLevel.Int())
|
||||
|
||||
// TODO check that daemon is either running as root, or that the
|
||||
// required linux capabilities are set.
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"isle/daemon"
|
||||
"isle/daemon/jsonrpc2"
|
||||
"net"
|
||||
@ -17,11 +18,25 @@ import (
|
||||
const daemonHTTPRPCPath = "/rpc/v0.json"
|
||||
|
||||
func newHTTPServer(
|
||||
ctx context.Context, logger *mlog.Logger, rpc *daemon.RPC,
|
||||
ctx context.Context, logger *mlog.Logger, rpc daemon.RPC,
|
||||
) (
|
||||
*http.Server, error,
|
||||
) {
|
||||
socketPath := daemon.HTTPSocketPath()
|
||||
ctx = mctx.Annotate(ctx, "socketPath", socketPath)
|
||||
|
||||
if err := os.Remove(socketPath); errors.Is(err, fs.ErrNotExist) {
|
||||
// No problem
|
||||
} else if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"removing %q prior to listening: %w", socketPath, err,
|
||||
)
|
||||
} else {
|
||||
logger.WarnString(
|
||||
ctx, "Deleted existing socket file prior to listening, it's possible a previous daemon failed to shutdown gracefully",
|
||||
)
|
||||
}
|
||||
|
||||
l, err := net.Listen("unix", socketPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
@ -35,7 +50,6 @@ func newHTTPServer(
|
||||
)
|
||||
}
|
||||
|
||||
ctx = mctx.Annotate(ctx, "httpAddr", l.Addr().String())
|
||||
logger.Info(ctx, "HTTP server socket created")
|
||||
|
||||
rpcHandler := jsonrpc2.Chain(
|
||||
|
@ -3,20 +3,37 @@ package main
|
||||
import (
|
||||
"encoding"
|
||||
"fmt"
|
||||
"isle/nebula"
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
type textUnmarshalerFlag struct {
|
||||
inner interface {
|
||||
encoding.TextUnmarshaler
|
||||
type textUnmarshaler[T any] interface {
|
||||
encoding.TextUnmarshaler
|
||||
*T
|
||||
}
|
||||
|
||||
type textUnmarshalerFlag[T encoding.TextMarshaler, P textUnmarshaler[T]] struct {
|
||||
V T
|
||||
}
|
||||
|
||||
func (f *textUnmarshalerFlag[T, P]) Set(v string) error {
|
||||
return P(&(f.V)).UnmarshalText([]byte(v))
|
||||
}
|
||||
|
||||
func (f *textUnmarshalerFlag[T, P]) String() string {
|
||||
b, err := f.V.MarshalText()
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("calling MarshalText on %#v: %v", f.V, err))
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func (f textUnmarshalerFlag) Set(v string) error {
|
||||
return f.inner.UnmarshalText([]byte(v))
|
||||
}
|
||||
func (f *textUnmarshalerFlag[T, P]) Type() string { return "string" }
|
||||
|
||||
func (f textUnmarshalerFlag) String() string {
|
||||
return fmt.Sprint(f.inner)
|
||||
}
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
func (f textUnmarshalerFlag) Type() string { return "string" }
|
||||
type (
|
||||
hostNameFlag = textUnmarshalerFlag[nebula.HostName, *nebula.HostName]
|
||||
ipNetFlag = textUnmarshalerFlag[nebula.IPNet, *nebula.IPNet]
|
||||
ipFlag = textUnmarshalerFlag[netip.Addr, *netip.Addr]
|
||||
)
|
||||
|
@ -6,8 +6,6 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"isle/daemon"
|
||||
)
|
||||
|
||||
// minio-client keeps a configuration directory which contains various pieces of
|
||||
@ -35,9 +33,9 @@ func initMCConfigDir() (string, error) {
|
||||
var subCmdGarageMC = subCmd{
|
||||
name: "mc",
|
||||
descr: "Runs the mc (minio-client) binary. The isle garage can be accessed under the `garage` alias",
|
||||
do: func(subCmdCtx subCmdCtx) error {
|
||||
do: func(ctx subCmdCtx) error {
|
||||
|
||||
flags := subCmdCtx.flagSet(true)
|
||||
flags := ctx.flagSet(true)
|
||||
|
||||
keyID := flags.StringP(
|
||||
"key-id", "i", "",
|
||||
@ -49,14 +47,11 @@ var subCmdGarageMC = subCmd{
|
||||
"Optional key secret to use, defaults to that of the shared global key",
|
||||
)
|
||||
|
||||
if err := flags.Parse(subCmdCtx.args); err != nil {
|
||||
if err := flags.Parse(ctx.args); err != nil {
|
||||
return fmt.Errorf("parsing flags: %w", err)
|
||||
}
|
||||
|
||||
var clientParams daemon.GarageClientParams
|
||||
err := subCmdCtx.daemonRCPClient.Call(
|
||||
subCmdCtx.ctx, &clientParams, "GetGarageClientParams", nil,
|
||||
)
|
||||
clientParams, err := ctx.daemonRPC.GetGarageClientParams(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("calling GetGarageClientParams: %w", err)
|
||||
}
|
||||
@ -118,12 +113,9 @@ var subCmdGarageMC = subCmd{
|
||||
var subCmdGarageCLI = subCmd{
|
||||
name: "cli",
|
||||
descr: "Runs the garage binary, automatically configured to point to the garage sub-process of a running isle daemon",
|
||||
do: func(subCmdCtx subCmdCtx) error {
|
||||
do: func(ctx subCmdCtx) error {
|
||||
|
||||
var clientParams daemon.GarageClientParams
|
||||
err := subCmdCtx.daemonRCPClient.Call(
|
||||
subCmdCtx.ctx, &clientParams, "GetGarageClientParams", nil,
|
||||
)
|
||||
clientParams, err := ctx.daemonRPC.GetGarageClientParams(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("calling GetGarageClientParams: %w", err)
|
||||
}
|
||||
@ -134,7 +126,7 @@ var subCmdGarageCLI = subCmd{
|
||||
|
||||
var (
|
||||
binPath = binPath("garage")
|
||||
args = append([]string{"garage"}, subCmdCtx.args...)
|
||||
args = append([]string{"garage"}, ctx.args...)
|
||||
cliEnv = append(
|
||||
os.Environ(),
|
||||
"GARAGE_RPC_HOST="+clientParams.Peer.RPCPeerAddr(),
|
||||
@ -156,8 +148,8 @@ var subCmdGarageCLI = subCmd{
|
||||
var subCmdGarage = subCmd{
|
||||
name: "garage",
|
||||
descr: "Runs the garage binary, automatically configured to point to the garage sub-process of a running isle daemon",
|
||||
do: func(subCmdCtx subCmdCtx) error {
|
||||
return subCmdCtx.doSubCmd(
|
||||
do: func(ctx subCmdCtx) error {
|
||||
return ctx.doSubCmd(
|
||||
subCmdGarageCLI,
|
||||
subCmdGarageMC,
|
||||
)
|
||||
|
@ -7,31 +7,27 @@ import (
|
||||
"isle/bootstrap"
|
||||
"isle/daemon"
|
||||
"isle/jsonutil"
|
||||
"isle/nebula"
|
||||
"net/netip"
|
||||
"os"
|
||||
"sort"
|
||||
)
|
||||
|
||||
var subCmdHostsCreate = subCmd{
|
||||
var subCmdHostCreate = subCmd{
|
||||
name: "create",
|
||||
descr: "Creates a new host in the network, writing its new bootstrap.json to stdout",
|
||||
do: func(subCmdCtx subCmdCtx) error {
|
||||
do: func(ctx subCmdCtx) error {
|
||||
var (
|
||||
flags = subCmdCtx.flagSet(false)
|
||||
hostName nebula.HostName
|
||||
ip netip.Addr
|
||||
flags = ctx.flagSet(false)
|
||||
hostName hostNameFlag
|
||||
ip ipFlag
|
||||
)
|
||||
|
||||
hostNameF := flags.VarPF(
|
||||
textUnmarshalerFlag{&hostName},
|
||||
"hostname", "h",
|
||||
&hostName,
|
||||
"hostname", "n",
|
||||
"Name of the host to generate bootstrap.json for",
|
||||
)
|
||||
|
||||
flags.VarP(
|
||||
textUnmarshalerFlag{&ip}, "ip", "i", "IP of the new host",
|
||||
)
|
||||
flags.VarP(&ip, "ip", "i", "IP of the new host. An available IP will be chosen if none is given.")
|
||||
|
||||
canCreateHosts := flags.Bool(
|
||||
"can-create-hosts",
|
||||
@ -39,7 +35,7 @@ var subCmdHostsCreate = subCmd{
|
||||
"The new host should have the ability to create hosts too",
|
||||
)
|
||||
|
||||
if err := flags.Parse(subCmdCtx.args); err != nil {
|
||||
if err := flags.Parse(ctx.args); err != nil {
|
||||
return fmt.Errorf("parsing flags: %w", err)
|
||||
}
|
||||
|
||||
@ -47,17 +43,10 @@ var subCmdHostsCreate = subCmd{
|
||||
return errors.New("--hostname is required")
|
||||
}
|
||||
|
||||
var res daemon.CreateHostResult
|
||||
err := subCmdCtx.daemonRCPClient.Call(
|
||||
subCmdCtx.ctx,
|
||||
&res,
|
||||
"CreateHost",
|
||||
daemon.CreateHostRequest{
|
||||
HostName: hostName,
|
||||
Opts: daemon.CreateHostOpts{
|
||||
IP: ip,
|
||||
CanCreateHosts: *canCreateHosts,
|
||||
},
|
||||
res, err := ctx.daemonRPC.CreateHost(
|
||||
ctx, hostName.V, daemon.CreateHostOpts{
|
||||
IP: ip.V,
|
||||
CanCreateHosts: *canCreateHosts,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
@ -68,11 +57,11 @@ var subCmdHostsCreate = subCmd{
|
||||
},
|
||||
}
|
||||
|
||||
var subCmdHostsList = subCmd{
|
||||
var subCmdHostList = subCmd{
|
||||
name: "list",
|
||||
descr: "Lists all hosts in the network, and their IPs",
|
||||
do: func(subCmdCtx subCmdCtx) error {
|
||||
hostsRes, err := subCmdCtx.getHosts()
|
||||
do: func(ctx subCmdCtx) error {
|
||||
hostsRes, err := ctx.getHosts()
|
||||
if err != nil {
|
||||
return fmt.Errorf("calling GetHosts: %w", err)
|
||||
}
|
||||
@ -104,22 +93,22 @@ var subCmdHostsList = subCmd{
|
||||
},
|
||||
}
|
||||
|
||||
var subCmdHostsRemove = subCmd{
|
||||
var subCmdHostRemove = subCmd{
|
||||
name: "remove",
|
||||
descr: "Removes a host from the network",
|
||||
do: func(subCmdCtx subCmdCtx) error {
|
||||
do: func(ctx subCmdCtx) error {
|
||||
var (
|
||||
flags = subCmdCtx.flagSet(false)
|
||||
hostName nebula.HostName
|
||||
flags = ctx.flagSet(false)
|
||||
hostName hostNameFlag
|
||||
)
|
||||
|
||||
hostNameF := flags.VarPF(
|
||||
textUnmarshalerFlag{&hostName},
|
||||
"hostname", "h",
|
||||
&hostName,
|
||||
"hostname", "n",
|
||||
"Name of the host to remove",
|
||||
)
|
||||
|
||||
if err := flags.Parse(subCmdCtx.args); err != nil {
|
||||
if err := flags.Parse(ctx.args); err != nil {
|
||||
return fmt.Errorf("parsing flags: %w", err)
|
||||
}
|
||||
|
||||
@ -127,11 +116,7 @@ var subCmdHostsRemove = subCmd{
|
||||
return errors.New("--hostname is required")
|
||||
}
|
||||
|
||||
err := subCmdCtx.daemonRCPClient.Call(
|
||||
subCmdCtx.ctx, nil, "RemoveHost", daemon.RemoveHostRequest{
|
||||
HostName: hostName,
|
||||
},
|
||||
)
|
||||
_, err := ctx.daemonRPC.RemoveHost(ctx, hostName.V)
|
||||
if err != nil {
|
||||
return fmt.Errorf("calling RemoveHost: %w", err)
|
||||
}
|
||||
@ -140,14 +125,15 @@ var subCmdHostsRemove = subCmd{
|
||||
},
|
||||
}
|
||||
|
||||
var subCmdHosts = subCmd{
|
||||
name: "hosts",
|
||||
descr: "Sub-commands having to do with configuration of hosts in the network",
|
||||
do: func(subCmdCtx subCmdCtx) error {
|
||||
return subCmdCtx.doSubCmd(
|
||||
subCmdHostsCreate,
|
||||
subCmdHostsRemove,
|
||||
subCmdHostsList,
|
||||
var subCmdHost = subCmd{
|
||||
name: "host",
|
||||
plural: "s",
|
||||
descr: "Sub-commands having to do with configuration of hosts in the network",
|
||||
do: func(ctx subCmdCtx) error {
|
||||
return ctx.doSubCmd(
|
||||
subCmdHostCreate,
|
||||
subCmdHostRemove,
|
||||
subCmdHostList,
|
||||
)
|
||||
},
|
||||
}
|
@ -57,13 +57,13 @@ func main() {
|
||||
}()
|
||||
|
||||
err := subCmdCtx{
|
||||
args: os.Args[1:],
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
Context: ctx,
|
||||
args: os.Args[1:],
|
||||
logger: logger,
|
||||
}.doSubCmd(
|
||||
subCmdDaemon,
|
||||
subCmdGarage,
|
||||
subCmdHosts,
|
||||
subCmdHost,
|
||||
subCmdNebula,
|
||||
subCmdNetwork,
|
||||
subCmdVersion,
|
||||
|
@ -3,7 +3,6 @@ package main
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"isle/daemon"
|
||||
"isle/jsonutil"
|
||||
"isle/nebula"
|
||||
"os"
|
||||
@ -12,15 +11,15 @@ import (
|
||||
var subCmdNebulaCreateCert = subCmd{
|
||||
name: "create-cert",
|
||||
descr: "Creates a signed nebula certificate file for an existing host and writes it to stdout",
|
||||
do: func(subCmdCtx subCmdCtx) error {
|
||||
do: func(ctx subCmdCtx) error {
|
||||
var (
|
||||
flags = subCmdCtx.flagSet(false)
|
||||
hostName nebula.HostName
|
||||
flags = ctx.flagSet(false)
|
||||
hostName hostNameFlag
|
||||
)
|
||||
|
||||
hostNameF := flags.VarPF(
|
||||
textUnmarshalerFlag{&hostName},
|
||||
"hostname", "h",
|
||||
&hostName,
|
||||
"hostname", "n",
|
||||
"Name of the host to generate a certificate for",
|
||||
)
|
||||
|
||||
@ -29,7 +28,7 @@ var subCmdNebulaCreateCert = subCmd{
|
||||
`Path to PEM file containing public key which will be embedded in the cert.`,
|
||||
)
|
||||
|
||||
if err := flags.Parse(subCmdCtx.args); err != nil {
|
||||
if err := flags.Parse(ctx.args); err != nil {
|
||||
return fmt.Errorf("parsing flags: %w", err)
|
||||
}
|
||||
|
||||
@ -47,21 +46,14 @@ var subCmdNebulaCreateCert = subCmd{
|
||||
return fmt.Errorf("unmarshaling public key as PEM: %w", err)
|
||||
}
|
||||
|
||||
var res daemon.CreateNebulaCertificateResult
|
||||
err = subCmdCtx.daemonRCPClient.Call(
|
||||
subCmdCtx.ctx,
|
||||
&res,
|
||||
"CreateNebulaCertificate",
|
||||
daemon.CreateNebulaCertificateRequest{
|
||||
HostName: hostName,
|
||||
HostEncryptingPublicKey: hostPub,
|
||||
},
|
||||
res, err := ctx.daemonRPC.CreateNebulaCertificate(
|
||||
ctx, hostName.V, hostPub,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("calling CreateNebulaCertificate: %w", err)
|
||||
}
|
||||
|
||||
nebulaHostCertPEM, err := res.HostNebulaCertifcate.Unwrap().MarshalToPEM()
|
||||
nebulaHostCertPEM, err := res.HostNebulaCertificate.Unwrap().MarshalToPEM()
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshaling cert to PEM: %w", err)
|
||||
}
|
||||
@ -77,22 +69,19 @@ var subCmdNebulaCreateCert = subCmd{
|
||||
var subCmdNebulaShow = subCmd{
|
||||
name: "show",
|
||||
descr: "Writes nebula network information to stdout in JSON format",
|
||||
do: func(subCmdCtx subCmdCtx) error {
|
||||
do: func(ctx subCmdCtx) error {
|
||||
|
||||
flags := subCmdCtx.flagSet(false)
|
||||
if err := flags.Parse(subCmdCtx.args); err != nil {
|
||||
flags := ctx.flagSet(false)
|
||||
if err := flags.Parse(ctx.args); err != nil {
|
||||
return fmt.Errorf("parsing flags: %w", err)
|
||||
}
|
||||
|
||||
hosts, err := subCmdCtx.getHosts()
|
||||
hosts, err := ctx.getHosts()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting hosts: %w", err)
|
||||
}
|
||||
|
||||
var caPublicCreds nebula.CAPublicCredentials
|
||||
err = subCmdCtx.daemonRCPClient.Call(
|
||||
subCmdCtx.ctx, &caPublicCreds, "GetNebulaCAPublicCredentials", nil,
|
||||
)
|
||||
caPublicCreds, err := ctx.daemonRPC.GetNebulaCAPublicCredentials(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("calling GetNebulaCAPublicCredentials: %w", err)
|
||||
}
|
||||
@ -145,8 +134,8 @@ var subCmdNebulaShow = subCmd{
|
||||
var subCmdNebula = subCmd{
|
||||
name: "nebula",
|
||||
descr: "Sub-commands related to the nebula VPN",
|
||||
do: func(subCmdCtx subCmdCtx) error {
|
||||
return subCmdCtx.doSubCmd(
|
||||
do: func(ctx subCmdCtx) error {
|
||||
return ctx.doSubCmd(
|
||||
subCmdNebulaCreateCert,
|
||||
subCmdNebulaShow,
|
||||
)
|
||||
|
@ -10,48 +10,50 @@ import (
|
||||
var subCmdNetworkCreate = subCmd{
|
||||
name: "create",
|
||||
descr: "Create's a new network, with this host being the first host in that network.",
|
||||
do: func(subCmdCtx subCmdCtx) error {
|
||||
do: func(ctx subCmdCtx) error {
|
||||
var (
|
||||
ctx = subCmdCtx.ctx
|
||||
flags = subCmdCtx.flagSet(false)
|
||||
req daemon.CreateNetworkRequest
|
||||
flags = ctx.flagSet(false)
|
||||
ipNet ipNetFlag
|
||||
hostName hostNameFlag
|
||||
)
|
||||
|
||||
flags.StringVarP(
|
||||
&req.Name, "name", "n", "",
|
||||
name := flags.StringP(
|
||||
"name", "N", "",
|
||||
"Human-readable name to identify the network as.",
|
||||
)
|
||||
|
||||
flags.StringVarP(
|
||||
&req.Domain, "domain", "d", "",
|
||||
domain := flags.StringP(
|
||||
"domain", "d", "",
|
||||
"Domain name that should be used as the root domain in the network.",
|
||||
)
|
||||
|
||||
ipNetF := flags.VarPF(
|
||||
textUnmarshalerFlag{&req.IPNet}, "ip-net", "i",
|
||||
&ipNet, "ip-net", "i",
|
||||
`An IP subnet, in CIDR form, which will be the overall range of`+
|
||||
` possible IPs in the network. The first IP in this network`+
|
||||
` range will become this first host's IP.`,
|
||||
)
|
||||
|
||||
hostNameF := flags.VarPF(
|
||||
textUnmarshalerFlag{&req.HostName},
|
||||
"hostname", "h",
|
||||
&hostName,
|
||||
"hostname", "n",
|
||||
"Name of this host, which will be the first host in the network",
|
||||
)
|
||||
|
||||
if err := flags.Parse(subCmdCtx.args); err != nil {
|
||||
if err := flags.Parse(ctx.args); err != nil {
|
||||
return fmt.Errorf("parsing flags: %w", err)
|
||||
}
|
||||
|
||||
if req.Name == "" ||
|
||||
req.Domain == "" ||
|
||||
if *name == "" ||
|
||||
*domain == "" ||
|
||||
!ipNetF.Changed ||
|
||||
!hostNameF.Changed {
|
||||
return errors.New("--name, --domain, --ip-net, and --hostname are required")
|
||||
}
|
||||
|
||||
err := subCmdCtx.daemonRCPClient.Call(ctx, nil, "CreateNetwork", req)
|
||||
_, err := ctx.daemonRPC.CreateNetwork(
|
||||
ctx, *name, *domain, ipNet.V, hostName.V,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating network: %w", err)
|
||||
}
|
||||
@ -63,17 +65,15 @@ var subCmdNetworkCreate = subCmd{
|
||||
var subCmdNetworkJoin = subCmd{
|
||||
name: "join",
|
||||
descr: "Joins this host to an existing network",
|
||||
do: func(subCmdCtx subCmdCtx) error {
|
||||
do: func(ctx subCmdCtx) error {
|
||||
var (
|
||||
ctx = subCmdCtx.ctx
|
||||
flags = subCmdCtx.flagSet(false)
|
||||
flags = ctx.flagSet(false)
|
||||
bootstrapPath = flags.StringP(
|
||||
"bootstrap-path", "b", "", "Path to a bootstrap.json file.",
|
||||
)
|
||||
)
|
||||
|
||||
bootstrapPath := flags.StringP(
|
||||
"bootstrap-path", "b", "", "Path to a bootstrap.json file.",
|
||||
)
|
||||
|
||||
if err := flags.Parse(subCmdCtx.args); err != nil {
|
||||
if err := flags.Parse(ctx.args); err != nil {
|
||||
return fmt.Errorf("parsing flags: %w", err)
|
||||
}
|
||||
|
||||
@ -88,17 +88,16 @@ var subCmdNetworkJoin = subCmd{
|
||||
)
|
||||
}
|
||||
|
||||
return subCmdCtx.daemonRCPClient.Call(
|
||||
ctx, nil, "JoinNetwork", newBootstrap,
|
||||
)
|
||||
_, err := ctx.daemonRPC.JoinNetwork(ctx, newBootstrap)
|
||||
return err
|
||||
},
|
||||
}
|
||||
|
||||
var subCmdNetwork = subCmd{
|
||||
name: "network",
|
||||
descr: "Sub-commands related to network membership",
|
||||
do: func(subCmdCtx subCmdCtx) error {
|
||||
return subCmdCtx.doSubCmd(
|
||||
do: func(ctx subCmdCtx) error {
|
||||
return ctx.doSubCmd(
|
||||
subCmdNetworkCreate,
|
||||
subCmdNetworkJoin,
|
||||
)
|
||||
|
@ -12,21 +12,41 @@ import (
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
type flagSet struct {
|
||||
*pflag.FlagSet
|
||||
}
|
||||
|
||||
func (fs flagSet) Parse(args []string) error {
|
||||
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
|
||||
|
||||
ctx context.Context
|
||||
logger *mlog.Logger
|
||||
daemonRCPClient jsonrpc2.Client
|
||||
logger *mlog.Logger
|
||||
daemonRPC daemon.RPC
|
||||
}
|
||||
|
||||
type subCmd struct {
|
||||
name string
|
||||
descr string
|
||||
do func(subCmdCtx) error
|
||||
|
||||
// If set then the name will be allowed to be suffixed with this string.
|
||||
plural string
|
||||
}
|
||||
|
||||
func (ctx subCmdCtx) usagePrefix() string {
|
||||
@ -39,7 +59,7 @@ func (ctx subCmdCtx) usagePrefix() string {
|
||||
return fmt.Sprintf("\nUSAGE: %s %s", os.Args[0], subCmdNamesStr)
|
||||
}
|
||||
|
||||
func (ctx subCmdCtx) flagSet(withPassthrough bool) *pflag.FlagSet {
|
||||
func (ctx subCmdCtx) flagSet(withPassthrough bool) flagSet {
|
||||
flags := pflag.NewFlagSet(ctx.subCmd.name, pflag.ExitOnError)
|
||||
flags.Usage = func() {
|
||||
|
||||
@ -58,7 +78,7 @@ func (ctx subCmdCtx) flagSet(withPassthrough bool) *pflag.FlagSet {
|
||||
os.Stderr.Sync()
|
||||
os.Exit(2)
|
||||
}
|
||||
return flags
|
||||
return flagSet{flags}
|
||||
}
|
||||
|
||||
func (ctx subCmdCtx) doSubCmd(subCmds ...subCmd) error {
|
||||
@ -76,7 +96,11 @@ func (ctx subCmdCtx) doSubCmd(subCmds ...subCmd) error {
|
||||
fmt.Fprintf(os.Stderr, "\nSUB-COMMANDS:\n\n")
|
||||
|
||||
for _, subCmd := range subCmds {
|
||||
fmt.Fprintf(os.Stderr, " %s\t%s\n", subCmd.name, subCmd.descr)
|
||||
name := subCmd.name
|
||||
if subCmd.plural != "" {
|
||||
name += "(" + subCmd.plural + ")"
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, " %s\t%s\n", name, subCmd.descr)
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
@ -92,8 +116,10 @@ func (ctx subCmdCtx) doSubCmd(subCmds ...subCmd) error {
|
||||
|
||||
subCmdsMap := map[string]subCmd{}
|
||||
for _, subCmd := range subCmds {
|
||||
// TODO allow subCmd(s) in some cases
|
||||
subCmdsMap[subCmd.name] = subCmd
|
||||
if subCmd.plural != "" {
|
||||
subCmdsMap[subCmd.name+subCmd.plural] = subCmd
|
||||
}
|
||||
}
|
||||
|
||||
subCmdName, args := args[0], args[1:]
|
||||
@ -103,17 +129,17 @@ func (ctx subCmdCtx) doSubCmd(subCmds ...subCmd) error {
|
||||
printUsageExit(subCmdName)
|
||||
}
|
||||
|
||||
daemonRCPClient := jsonrpc2.NewUnixHTTPClient(
|
||||
daemon.HTTPSocketPath(), daemonHTTPRPCPath,
|
||||
daemonRPC := daemon.RPCFromClient(
|
||||
jsonrpc2.NewUnixHTTPClient(daemon.HTTPSocketPath(), daemonHTTPRPCPath),
|
||||
)
|
||||
|
||||
err := subCmd.do(subCmdCtx{
|
||||
subCmd: subCmd,
|
||||
args: args,
|
||||
subCmdNames: append(ctx.subCmdNames, subCmdName),
|
||||
ctx: ctx.ctx,
|
||||
logger: ctx.logger,
|
||||
daemonRCPClient: daemonRCPClient,
|
||||
Context: ctx.Context,
|
||||
subCmd: subCmd,
|
||||
args: args,
|
||||
subCmdNames: append(ctx.subCmdNames, subCmdName),
|
||||
logger: ctx.logger,
|
||||
daemonRPC: daemonRPC,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
|
@ -9,7 +9,7 @@ import (
|
||||
var subCmdVersion = subCmd{
|
||||
name: "version",
|
||||
descr: "Dumps version and build info to stdout",
|
||||
do: func(subCmdCtx subCmdCtx) error {
|
||||
do: func(ctx subCmdCtx) error {
|
||||
|
||||
versionPath := filepath.Join(envAppDirPath, "share/version")
|
||||
|
||||
|
@ -75,6 +75,14 @@ func dnsmasqPmuxProcConfig(
|
||||
Cmd: filepath.Join(binDirPath, "dnsmasq"),
|
||||
Args: []string{"-d", "-C", confPath},
|
||||
StartAfterFunc: func(ctx context.Context) error {
|
||||
// TODO consider a shared dnsmasq across all the daemon's networks.
|
||||
// This would have a few benefits:
|
||||
// - Less processes, less problems
|
||||
// - Less configuration for the user in the case of more than one
|
||||
// network.
|
||||
// - Can listen on 127.0.0.x:53, rather than on the nebula address.
|
||||
// This allows DNS to come up before nebula, which is helpful when
|
||||
// nebula depends on DNS.
|
||||
return waitForNebula(ctx, logger, hostBootstrap)
|
||||
},
|
||||
}, nil
|
||||
|
108
go/daemon/client.go
Normal file
108
go/daemon/client.go
Normal file
@ -0,0 +1,108 @@
|
||||
// Code generated by gowrap. DO NOT EDIT.
|
||||
// template: jsonrpc2/client_gen.tpl
|
||||
// gowrap: http://github.com/hexdigest/gowrap
|
||||
|
||||
package daemon
|
||||
|
||||
//go:generate gowrap gen -p isle/daemon -i RPC -t jsonrpc2/client_gen.tpl -o client.go -l ""
|
||||
|
||||
import (
|
||||
"context"
|
||||
"isle/daemon/jsonrpc2"
|
||||
"isle/nebula"
|
||||
)
|
||||
|
||||
type rpcClient struct {
|
||||
client jsonrpc2.Client
|
||||
}
|
||||
|
||||
// RPCFromClient wraps a Client so that it implements the
|
||||
// RPC interface.
|
||||
func RPCFromClient(client jsonrpc2.Client) RPC {
|
||||
return &rpcClient{client}
|
||||
}
|
||||
|
||||
func (c *rpcClient) CreateHost(ctx context.Context, hostName nebula.HostName, opts CreateHostOpts) (c2 CreateHostResult, err error) {
|
||||
err = c.client.Call(
|
||||
ctx,
|
||||
&c2,
|
||||
"CreateHost",
|
||||
hostName,
|
||||
opts,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *rpcClient) CreateNebulaCertificate(ctx context.Context, hostName nebula.HostName, hostEncryptingPublicKey nebula.EncryptingPublicKey) (c2 CreateNebulaCertificateResult, err error) {
|
||||
err = c.client.Call(
|
||||
ctx,
|
||||
&c2,
|
||||
"CreateNebulaCertificate",
|
||||
hostName,
|
||||
hostEncryptingPublicKey,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *rpcClient) CreateNetwork(ctx context.Context, name string, domain string, ipNet nebula.IPNet, hostName nebula.HostName) (st1 struct {
|
||||
}, err error) {
|
||||
err = c.client.Call(
|
||||
ctx,
|
||||
&st1,
|
||||
"CreateNetwork",
|
||||
name,
|
||||
domain,
|
||||
ipNet,
|
||||
hostName,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *rpcClient) GetGarageClientParams(ctx context.Context) (g1 GarageClientParams, err error) {
|
||||
err = c.client.Call(
|
||||
ctx,
|
||||
&g1,
|
||||
"GetGarageClientParams",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *rpcClient) GetHosts(ctx context.Context) (g1 GetHostsResult, err error) {
|
||||
err = c.client.Call(
|
||||
ctx,
|
||||
&g1,
|
||||
"GetHosts",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *rpcClient) GetNebulaCAPublicCredentials(ctx context.Context) (c2 nebula.CAPublicCredentials, err error) {
|
||||
err = c.client.Call(
|
||||
ctx,
|
||||
&c2,
|
||||
"GetNebulaCAPublicCredentials",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *rpcClient) JoinNetwork(ctx context.Context, req JoiningBootstrap) (st1 struct {
|
||||
}, err error) {
|
||||
err = c.client.Call(
|
||||
ctx,
|
||||
&st1,
|
||||
"JoinNetwork",
|
||||
req,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *rpcClient) RemoveHost(ctx context.Context, hostName nebula.HostName) (st1 struct {
|
||||
}, err error) {
|
||||
err = c.client.Call(
|
||||
ctx,
|
||||
&st1,
|
||||
"RemoveHost",
|
||||
hostName,
|
||||
)
|
||||
return
|
||||
}
|
@ -32,6 +32,8 @@ type CreateHostOpts struct {
|
||||
// CanCreateHosts indicates that the bootstrap produced by CreateHost should
|
||||
// give the new host the ability to create new hosts as well.
|
||||
CanCreateHosts bool
|
||||
|
||||
// TODO add nebula cert tags
|
||||
}
|
||||
|
||||
// Daemon presents all functionality required for client frontends to interact
|
||||
@ -85,6 +87,9 @@ type Daemon interface {
|
||||
// existing host, given the public key for that host. This is currently
|
||||
// mostly useful for creating certs for mobile devices.
|
||||
//
|
||||
// TODO replace this with CreateHostBootstrap, and the
|
||||
// CreateNebulaCertificate RPC method can just pull cert out of that.
|
||||
//
|
||||
// Errors:
|
||||
// - ErrHostNotFound
|
||||
CreateNebulaCertificate(
|
||||
@ -685,6 +690,7 @@ func (d *daemon) CreateHost(
|
||||
)
|
||||
}
|
||||
}
|
||||
// TODO if the ip is given, check that it's not already in use.
|
||||
|
||||
caSigningPrivateKey, err := getNebulaCASigningPrivateKey(
|
||||
ctx, d.secretsStore,
|
||||
|
@ -15,5 +15,5 @@ type Client interface {
|
||||
//
|
||||
// If an error result is returned from the server that will be returned as
|
||||
// an Error struct.
|
||||
Call(ctx context.Context, rcv any, method string, params any) error
|
||||
Call(ctx context.Context, rcv any, method string, params ...any) error
|
||||
}
|
||||
|
32
go/daemon/jsonrpc2/client_gen.tpl
Normal file
32
go/daemon/jsonrpc2/client_gen.tpl
Normal file
@ -0,0 +1,32 @@
|
||||
import (
|
||||
"isle/daemon/jsonrpc2"
|
||||
)
|
||||
|
||||
{{ $t := printf "%sClient" (down .Interface.Name) }}
|
||||
|
||||
type {{$t}} struct {
|
||||
client jsonrpc2.Client
|
||||
}
|
||||
|
||||
// {{.Interface.Name}}FromClient wraps a Client so that it implements the
|
||||
// {{.Interface.Name}} interface.
|
||||
func {{.Interface.Name}}FromClient(client jsonrpc2.Client) {{.Interface.Name}} {
|
||||
return &{{$t}}{client}
|
||||
}
|
||||
|
||||
{{range $method := .Interface.Methods}}
|
||||
func (c *{{$t}}) {{$method.Declaration}} {
|
||||
{{- $ctx := (index $method.Params 0).Name}}
|
||||
{{- $rcv := (index $method.Results 0).Name}}
|
||||
{{- $err := (index $method.Results 1).Name}}
|
||||
{{- $err}} = c.client.Call(
|
||||
{{$ctx}},
|
||||
&{{$rcv}},
|
||||
"{{$method.Name}}",
|
||||
{{- range $param := (slice $method.Params 1)}}
|
||||
{{$param.Name}},
|
||||
{{- end}}
|
||||
)
|
||||
return
|
||||
}
|
||||
{{end}}
|
@ -49,7 +49,7 @@ func NewUnixHTTPClient(unixSocketPath, reqPath string) Client {
|
||||
}
|
||||
|
||||
func (c *httpClient) Call(
|
||||
ctx context.Context, rcv any, method string, params any,
|
||||
ctx context.Context, rcv any, method string, params ...any,
|
||||
) error {
|
||||
var (
|
||||
body = new(bytes.Buffer)
|
||||
|
@ -19,7 +19,7 @@ func NewReadWriterClient(rw io.ReadWriter) Client {
|
||||
}
|
||||
|
||||
func (c rwClient) Call(
|
||||
ctx context.Context, rcv any, method string, params any,
|
||||
ctx context.Context, rcv any, method string, params ...any,
|
||||
) error {
|
||||
id, err := encodeRequest(c.enc, method, params)
|
||||
if err != nil {
|
||||
|
@ -18,29 +18,37 @@ type methodDispatchFunc func(context.Context, Request) (any, error)
|
||||
func newMethodDispatchFunc(
|
||||
method reflect.Value,
|
||||
) methodDispatchFunc {
|
||||
paramT := method.Type().In(1)
|
||||
return func(ctx context.Context, req Request) (any, error) {
|
||||
var (
|
||||
ctxV = reflect.ValueOf(ctx)
|
||||
paramPtrV = reflect.New(paramT)
|
||||
)
|
||||
paramTs := make([]reflect.Type, method.Type().NumIn()-1)
|
||||
for i := range paramTs {
|
||||
paramTs[i] = method.Type().In(i + 1)
|
||||
}
|
||||
|
||||
err := json.Unmarshal(req.Params, paramPtrV.Interface())
|
||||
if err != nil {
|
||||
// The JSON has already been validated, so this is not an
|
||||
// errCodeParse situation. We assume it's an invalid param then,
|
||||
// unless the error says otherwise via an UnmarshalJSON method
|
||||
// returning an Error of its own.
|
||||
if !errors.As(err, new(Error)) {
|
||||
err = NewInvalidParamsError(
|
||||
"JSON unmarshaling params into %T: %v", paramT, err,
|
||||
)
|
||||
return func(ctx context.Context, req Request) (any, error) {
|
||||
callVals := make([]reflect.Value, 0, len(paramTs)+1)
|
||||
callVals = append(callVals, reflect.ValueOf(ctx))
|
||||
|
||||
for i, paramT := range paramTs {
|
||||
paramPtrV := reflect.New(paramT)
|
||||
|
||||
err := json.Unmarshal(req.Params[i], paramPtrV.Interface())
|
||||
if err != nil {
|
||||
// The JSON has already been validated, so this is not an
|
||||
// errCodeParse situation. We assume it's an invalid param then,
|
||||
// unless the error says otherwise via an UnmarshalJSON method
|
||||
// returning an Error of its own.
|
||||
if !errors.As(err, new(Error)) {
|
||||
err = NewInvalidParamsError(
|
||||
"JSON unmarshaling param %d into %T: %v", i, paramT, err,
|
||||
)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return nil, err
|
||||
|
||||
callVals = append(callVals, paramPtrV.Elem())
|
||||
}
|
||||
|
||||
var (
|
||||
callResV = method.Call([]reflect.Value{ctxV, paramPtrV.Elem()})
|
||||
callResV = method.Call(callVals)
|
||||
resV = callResV[0]
|
||||
errV = callResV[1]
|
||||
)
|
||||
@ -86,7 +94,7 @@ func NewDispatchHandler(i any) Handler {
|
||||
)
|
||||
|
||||
if !method.IsExported() ||
|
||||
methodT.NumIn() != 2 ||
|
||||
methodT.NumIn() < 1 ||
|
||||
methodT.In(0) != ctxT ||
|
||||
methodT.NumOut() != 2 ||
|
||||
methodT.Out(1) != errT {
|
||||
|
@ -3,6 +3,7 @@ package jsonrpc2
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"dev.mediocregopher.com/mediocre-go-lib.git/mctx"
|
||||
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
|
||||
@ -59,9 +60,14 @@ func NewMLogMiddleware(logger *mlog.Logger) Middleware {
|
||||
)
|
||||
|
||||
if logger.MaxLevel() >= mlog.LevelDebug.Int() {
|
||||
ctx := mctx.Annotate(
|
||||
ctx, "rpcRequestParams", string(req.Params),
|
||||
)
|
||||
ctx := ctx
|
||||
for i := range req.Params {
|
||||
ctx = mctx.Annotate(
|
||||
ctx,
|
||||
fmt.Sprintf("rpcRequestParam%d", i),
|
||||
string(req.Params[i]),
|
||||
)
|
||||
}
|
||||
logger.Debug(ctx, "Handling RPC request")
|
||||
}
|
||||
|
||||
|
@ -13,10 +13,10 @@ const version = "2.0"
|
||||
|
||||
// Request encodes an RPC request according to the spec.
|
||||
type Request struct {
|
||||
Version string `json:"jsonrpc"` // must be "2.0"
|
||||
Method string `json:"method"`
|
||||
Params json.RawMessage `json:"params,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Version string `json:"jsonrpc"` // must be "2.0"
|
||||
Method string `json:"method"`
|
||||
Params []json.RawMessage `json:"params,omitempty"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
type response[Result any] struct {
|
||||
@ -37,13 +37,19 @@ func newID() string {
|
||||
// encodeRequest writes a request to an io.Writer, returning the ID of the
|
||||
// request.
|
||||
func encodeRequest(
|
||||
enc *json.Encoder, method string, params any,
|
||||
enc *json.Encoder, method string, params []any,
|
||||
) (
|
||||
string, error,
|
||||
) {
|
||||
paramsB, err := json.Marshal(params)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("encoding params as JSON: %w", err)
|
||||
var (
|
||||
paramsBs = make([]json.RawMessage, len(params))
|
||||
err error
|
||||
)
|
||||
for i := range params {
|
||||
paramsBs[i], err = json.Marshal(params[i])
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("encoding param %d as JSON: %w", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
@ -51,7 +57,7 @@ func encodeRequest(
|
||||
reqEnvelope = Request{
|
||||
Version: version,
|
||||
Method: method,
|
||||
Params: paramsB,
|
||||
Params: paramsBs,
|
||||
ID: id,
|
||||
}
|
||||
)
|
||||
|
@ -26,14 +26,22 @@ var ErrDivideByZero = Error{
|
||||
|
||||
type dividerImpl struct{}
|
||||
|
||||
func (dividerImpl) Divide(ctx context.Context, p DivideParams) (int, error) {
|
||||
if p.Bottom == 0 {
|
||||
func (dividerImpl) Divide2(ctx context.Context, top, bottom int) (int, error) {
|
||||
if bottom == 0 {
|
||||
return 0, ErrDivideByZero
|
||||
}
|
||||
if p.Top%p.Bottom != 0 {
|
||||
if top%bottom != 0 {
|
||||
return 0, errors.New("numbers don't divide evenly, cannot compute!")
|
||||
}
|
||||
return p.Top / p.Bottom, nil
|
||||
return top / bottom, nil
|
||||
}
|
||||
|
||||
func (i dividerImpl) Noop(ctx context.Context) (int, error) {
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
func (i dividerImpl) Divide(ctx context.Context, p DivideParams) (int, error) {
|
||||
return i.Divide2(ctx, p.Top, p.Bottom)
|
||||
}
|
||||
|
||||
func (dividerImpl) Hidden(ctx context.Context, p struct{}) (int, error) {
|
||||
@ -41,6 +49,8 @@ func (dividerImpl) Hidden(ctx context.Context, p struct{}) (int, error) {
|
||||
}
|
||||
|
||||
type divider interface {
|
||||
Noop(ctx context.Context) (int, error)
|
||||
Divide2(ctx context.Context, top, bottom int) (int, error)
|
||||
Divide(ctx context.Context, p DivideParams) (int, error)
|
||||
}
|
||||
|
||||
@ -82,6 +92,26 @@ func testClient(t *testing.T, client Client) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("success/multiple_params", func(t *testing.T) {
|
||||
var res int
|
||||
err := client.Call(ctx, &res, "Divide2", 6, 3)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
} else if res != 2 {
|
||||
t.Fatalf("expected 2, got %d", res)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("success/no_params", func(t *testing.T) {
|
||||
var res int
|
||||
err := client.Call(ctx, &res, "Noop")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
} else if res != 1 {
|
||||
t.Fatalf("expected 1, got %d", res)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("err/application", func(t *testing.T) {
|
||||
err := client.Call(ctx, nil, "Divide", DivideParams{})
|
||||
if !errors.Is(err, ErrDivideByZero) {
|
||||
|
214
go/daemon/rpc.go
214
go/daemon/rpc.go
@ -11,49 +11,115 @@ import (
|
||||
"golang.org/x/exp/maps"
|
||||
)
|
||||
|
||||
// GetHostsResult wraps the results from the GetHosts RPC method.
|
||||
type GetHostsResult struct {
|
||||
Hosts []bootstrap.Host
|
||||
}
|
||||
|
||||
// CreateHostResult wraps the results from the CreateHost RPC method.
|
||||
type CreateHostResult struct {
|
||||
JoiningBootstrap JoiningBootstrap
|
||||
}
|
||||
|
||||
// CreateNebulaCertificateResult wraps the results from the
|
||||
// CreateNebulaCertificate RPC method.
|
||||
type CreateNebulaCertificateResult struct {
|
||||
HostNebulaCertificate nebula.Certificate
|
||||
}
|
||||
|
||||
// RPC exposes all RPC methods which are available to be called over the RPC
|
||||
// interface.
|
||||
type RPC struct {
|
||||
type RPC interface {
|
||||
// CreateNetwork passes through to the Daemon method of the same name.
|
||||
//
|
||||
// name: Human-readable name of the network.
|
||||
// domain: Primary domain name that network services are served under.
|
||||
// ipNet:
|
||||
// An IP subnet, in CIDR form, which will be the overall range of
|
||||
// possible IPs in the network. The first IP in this network range will
|
||||
// become this first host's IP.
|
||||
// hostName: The name of this first host in the network.
|
||||
CreateNetwork(
|
||||
ctx context.Context,
|
||||
name string,
|
||||
domain string,
|
||||
ipNet nebula.IPNet,
|
||||
hostName nebula.HostName,
|
||||
) (
|
||||
struct{}, error,
|
||||
)
|
||||
|
||||
// JoinNetwork passes through to the Daemon method of the same name.
|
||||
JoinNetwork(
|
||||
ctx context.Context, req JoiningBootstrap,
|
||||
) (
|
||||
struct{}, error,
|
||||
)
|
||||
|
||||
// GetHosts returns all hosts known to the network, sorted by their name.
|
||||
GetHosts(ctx context.Context) (GetHostsResult, error)
|
||||
|
||||
// GetGarageClientParams passes the call through to the Daemon method of the
|
||||
// same name.
|
||||
GetGarageClientParams(ctx context.Context) (GarageClientParams, error)
|
||||
|
||||
// GetNebulaCAPublicCredentials returns the CAPublicCredentials for the
|
||||
// network.
|
||||
GetNebulaCAPublicCredentials(
|
||||
ctx context.Context,
|
||||
) (
|
||||
nebula.CAPublicCredentials, error,
|
||||
)
|
||||
|
||||
// RemoveHost passes the call through to the Daemon method of the same name.
|
||||
RemoveHost(
|
||||
ctx context.Context, hostName nebula.HostName,
|
||||
) (
|
||||
struct{}, error,
|
||||
)
|
||||
|
||||
// CreateHost passes the call through to the Daemon method of the same name.
|
||||
CreateHost(
|
||||
ctx context.Context, hostName nebula.HostName, opts CreateHostOpts,
|
||||
) (
|
||||
CreateHostResult, error,
|
||||
)
|
||||
|
||||
// CreateNebulaCertificate passes the call through to the Daemon method of
|
||||
// the same name.
|
||||
CreateNebulaCertificate(
|
||||
ctx context.Context,
|
||||
hostName nebula.HostName,
|
||||
hostEncryptingPublicKey nebula.EncryptingPublicKey,
|
||||
) (
|
||||
CreateNebulaCertificateResult, error,
|
||||
)
|
||||
}
|
||||
|
||||
type rpcImpl struct {
|
||||
daemon Daemon
|
||||
}
|
||||
|
||||
// NewRPC initializes and returns an RPC instance.
|
||||
func NewRPC(daemon Daemon) *RPC {
|
||||
return &RPC{daemon}
|
||||
func NewRPC(daemon Daemon) RPC {
|
||||
return &rpcImpl{daemon}
|
||||
}
|
||||
|
||||
// CreateNetworkRequest contains the arguments to the CreateNetwork RPC method.
|
||||
//
|
||||
// All fields are required.
|
||||
type CreateNetworkRequest struct {
|
||||
// Human-readable name of the network.
|
||||
Name string
|
||||
|
||||
// Primary domain name that network services are served under.
|
||||
Domain string
|
||||
|
||||
// An IP subnet, in CIDR form, which will be the overall range of possible
|
||||
// IPs in the network. The first IP in this network range will become this
|
||||
// first host's IP.
|
||||
IPNet nebula.IPNet
|
||||
|
||||
// The name of this first host in the network.
|
||||
HostName nebula.HostName
|
||||
}
|
||||
|
||||
// CreateNetwork passes through to the Daemon method of the same name.
|
||||
func (r *RPC) CreateNetwork(
|
||||
ctx context.Context, req CreateNetworkRequest,
|
||||
func (r *rpcImpl) CreateNetwork(
|
||||
ctx context.Context,
|
||||
name string,
|
||||
domain string,
|
||||
ipNet nebula.IPNet,
|
||||
hostName nebula.HostName,
|
||||
) (
|
||||
struct{}, error,
|
||||
) {
|
||||
return struct{}{}, r.daemon.CreateNetwork(
|
||||
ctx, req.Name, req.Domain, req.IPNet, req.HostName,
|
||||
ctx, name, domain, ipNet, hostName,
|
||||
)
|
||||
}
|
||||
|
||||
// JoinNetwork passes through to the Daemon method of the same name.
|
||||
func (r *RPC) JoinNetwork(
|
||||
func (r *rpcImpl) JoinNetwork(
|
||||
ctx context.Context, req JoiningBootstrap,
|
||||
) (
|
||||
struct{}, error,
|
||||
@ -61,17 +127,7 @@ func (r *RPC) JoinNetwork(
|
||||
return struct{}{}, r.daemon.JoinNetwork(ctx, req)
|
||||
}
|
||||
|
||||
// GetHostsResult wraps the results from the GetHosts RPC method.
|
||||
type GetHostsResult struct {
|
||||
Hosts []bootstrap.Host
|
||||
}
|
||||
|
||||
// GetHosts returns all hosts known to the network, sorted by their name.
|
||||
func (r *RPC) GetHosts(
|
||||
ctx context.Context, req struct{},
|
||||
) (
|
||||
GetHostsResult, error,
|
||||
) {
|
||||
func (r *rpcImpl) GetHosts(ctx context.Context) (GetHostsResult, error) {
|
||||
b, err := r.daemon.GetBootstrap(ctx)
|
||||
if err != nil {
|
||||
return GetHostsResult{}, fmt.Errorf("retrieving bootstrap: %w", err)
|
||||
@ -85,19 +141,16 @@ func (r *RPC) GetHosts(
|
||||
return GetHostsResult{hosts}, nil
|
||||
}
|
||||
|
||||
// GetGarageClientParams passes the call through to the Daemon method of the
|
||||
// same name.
|
||||
func (r *RPC) GetGarageClientParams(
|
||||
ctx context.Context, req struct{},
|
||||
func (r *rpcImpl) GetGarageClientParams(
|
||||
ctx context.Context,
|
||||
) (
|
||||
GarageClientParams, error,
|
||||
) {
|
||||
return r.daemon.GetGarageClientParams(ctx)
|
||||
}
|
||||
|
||||
// GetNebulaCAPublicCredentials returns the CAPublicCredentials for the network.
|
||||
func (r *RPC) GetNebulaCAPublicCredentials(
|
||||
ctx context.Context, req struct{},
|
||||
func (r *rpcImpl) GetNebulaCAPublicCredentials(
|
||||
ctx context.Context,
|
||||
) (
|
||||
nebula.CAPublicCredentials, error,
|
||||
) {
|
||||
@ -111,42 +164,20 @@ func (r *RPC) GetNebulaCAPublicCredentials(
|
||||
return b.CAPublicCredentials, nil
|
||||
}
|
||||
|
||||
// RemoveHostRequest contains the arguments to the RemoveHost RPC method.
|
||||
//
|
||||
// All fields are required.
|
||||
type RemoveHostRequest struct {
|
||||
HostName nebula.HostName
|
||||
func (r *rpcImpl) RemoveHost(
|
||||
ctx context.Context, hostName nebula.HostName,
|
||||
) (
|
||||
struct{}, error,
|
||||
) {
|
||||
return struct{}{}, r.daemon.RemoveHost(ctx, hostName)
|
||||
}
|
||||
|
||||
// RemoveHost passes the call through to the Daemon method of the same name.
|
||||
func (r *RPC) RemoveHost(ctx context.Context, req RemoveHostRequest) (struct{}, error) {
|
||||
return struct{}{}, r.daemon.RemoveHost(ctx, req.HostName)
|
||||
}
|
||||
|
||||
// CreateHostRequest contains the arguments to the
|
||||
// CreateHost RPC method.
|
||||
//
|
||||
// All fields are required.
|
||||
type CreateHostRequest struct {
|
||||
HostName nebula.HostName
|
||||
Opts CreateHostOpts
|
||||
}
|
||||
|
||||
// CreateHostResult wraps the results from the CreateHost RPC method.
|
||||
type CreateHostResult struct {
|
||||
JoiningBootstrap JoiningBootstrap
|
||||
}
|
||||
|
||||
// CreateHost passes the call through to the Daemon method of the
|
||||
// same name.
|
||||
func (r *RPC) CreateHost(
|
||||
ctx context.Context, req CreateHostRequest,
|
||||
func (r *rpcImpl) CreateHost(
|
||||
ctx context.Context, hostName nebula.HostName, opts CreateHostOpts,
|
||||
) (
|
||||
CreateHostResult, error,
|
||||
) {
|
||||
joiningBootstrap, err := r.daemon.CreateHost(
|
||||
ctx, req.HostName, req.Opts,
|
||||
)
|
||||
joiningBootstrap, err := r.daemon.CreateHost(ctx, hostName, opts)
|
||||
if err != nil {
|
||||
return CreateHostResult{}, err
|
||||
}
|
||||
@ -154,36 +185,19 @@ func (r *RPC) CreateHost(
|
||||
return CreateHostResult{JoiningBootstrap: joiningBootstrap}, nil
|
||||
}
|
||||
|
||||
// CreateNebulaCertificateRequest contains the arguments to the
|
||||
// CreateNebulaCertificate RPC method.
|
||||
//
|
||||
// All fields are required.
|
||||
type CreateNebulaCertificateRequest struct {
|
||||
HostName nebula.HostName
|
||||
HostEncryptingPublicKey nebula.EncryptingPublicKey
|
||||
}
|
||||
|
||||
// CreateNebulaCertificateResult wraps the results from the
|
||||
// CreateNebulaCertificate RPC method.
|
||||
type CreateNebulaCertificateResult struct {
|
||||
HostNebulaCertifcate nebula.Certificate
|
||||
}
|
||||
|
||||
// CreateNebulaCertificate passes the call through to the Daemon method of the
|
||||
// same name.
|
||||
func (r *RPC) CreateNebulaCertificate(
|
||||
ctx context.Context, req CreateNebulaCertificateRequest,
|
||||
func (r *rpcImpl) CreateNebulaCertificate(
|
||||
ctx context.Context,
|
||||
hostName nebula.HostName,
|
||||
hostEncryptingPublicKey nebula.EncryptingPublicKey,
|
||||
) (
|
||||
CreateNebulaCertificateResult, error,
|
||||
) {
|
||||
cert, err := r.daemon.CreateNebulaCertificate(
|
||||
ctx, req.HostName, req.HostEncryptingPublicKey,
|
||||
ctx, hostName, hostEncryptingPublicKey,
|
||||
)
|
||||
if err != nil {
|
||||
return CreateNebulaCertificateResult{}, err
|
||||
}
|
||||
|
||||
return CreateNebulaCertificateResult{
|
||||
HostNebulaCertifcate: cert,
|
||||
}, nil
|
||||
return CreateNebulaCertificateResult{HostNebulaCertificate: cert}, nil
|
||||
}
|
||||
|
@ -11,6 +11,11 @@ var hostNameRegexp = regexp.MustCompile(`^[a-z][a-z0-9\-]*$`)
|
||||
// lowercase letters, numbers, and hyphens, and must start with a letter.
|
||||
type HostName string
|
||||
|
||||
// MarshalText casts the HostName to a byte string and returns it.
|
||||
func (h HostName) MarshalText() ([]byte, error) {
|
||||
return []byte(h), nil
|
||||
}
|
||||
|
||||
// UnmarshalText parses and validates a HostName from a text string.
|
||||
func (h *HostName) UnmarshalText(b []byte) error {
|
||||
if !hostNameRegexp.Match(b) {
|
||||
|
17
nix/gowrap.nix
Normal file
17
nix/gowrap.nix
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
buildGoModule,
|
||||
fetchFromGitHub,
|
||||
}: let
|
||||
version = "1.4.0";
|
||||
in buildGoModule {
|
||||
pname = "gowrap";
|
||||
inherit version;
|
||||
src = fetchFromGitHub {
|
||||
owner = "hexdigest";
|
||||
repo = "gowrap";
|
||||
rev = "v${version}";
|
||||
hash = "sha256-eEaUANLnxRGfVbhOTwJV+R9iEWMObg0lHqmwO3AYuIk=";
|
||||
};
|
||||
vendorHash = "sha256-xIOyXXt8WSGQYIvIam+0e25VNI7awYEEYZBe7trC6zQ=";
|
||||
subPackages = [ "cmd/gowrap" ];
|
||||
}
|
Loading…
Reference in New Issue
Block a user