Compare commits

..

No commits in common. "038a28bb0275d65852cfe6dd7d8d75534c84ae9b" and "af69f1cfbac8b793e47a002d9006070571ab4400" have entirely different histories.

29 changed files with 427 additions and 565 deletions

View File

@ -105,4 +105,7 @@ 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 Normal file
View File

@ -0,0 +1,128 @@
# 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.

View File

@ -47,10 +47,6 @@ 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,

View File

@ -6,7 +6,8 @@ import (
)
func (ctx subCmdCtx) getHosts() (daemon.GetHostsResult, error) {
res, err := ctx.daemonRPC.GetHosts(ctx)
var res daemon.GetHostsResult
err := ctx.daemonRCPClient.Call(ctx.ctx, &res, "GetHosts", nil)
if err != nil {
return daemon.GetHostsResult{}, fmt.Errorf("calling GetHosts: %w", err)
}

View File

@ -10,16 +10,12 @@ 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(ctx subCmdCtx) error {
do: func(subCmdCtx subCmdCtx) error {
flags := ctx.flagSet(false)
flags := subCmdCtx.flagSet(false)
daemonConfigPath := flags.StringP(
"config-path", "c", "",
@ -36,10 +32,12 @@ 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(ctx.args); err != nil {
if err := flags.Parse(subCmdCtx.args); err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
ctx := subCmdCtx.ctx
if *dumpConfig {
return daemon.CopyDefaultConfig(os.Stdout, envAppDirPath)
}
@ -49,7 +47,7 @@ var subCmdDaemon = subCmd{
return fmt.Errorf("couldn't parse log level %q", *logLevelStr)
}
logger := ctx.logger.WithMaxLevel(logLevel.Int())
logger := subCmdCtx.logger.WithMaxLevel(logLevel.Int())
// TODO check that daemon is either running as root, or that the
// required linux capabilities are set.

View File

@ -4,7 +4,6 @@ import (
"context"
"errors"
"fmt"
"io/fs"
"isle/daemon"
"isle/daemon/jsonrpc2"
"net"
@ -18,25 +17,11 @@ 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(
@ -50,6 +35,7 @@ func newHTTPServer(
)
}
ctx = mctx.Annotate(ctx, "httpAddr", l.Addr().String())
logger.Info(ctx, "HTTP server socket created")
rpcHandler := jsonrpc2.Chain(

View File

@ -3,37 +3,20 @@ package main
import (
"encoding"
"fmt"
"isle/nebula"
"net/netip"
)
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))
type textUnmarshalerFlag struct {
inner interface {
encoding.TextUnmarshaler
}
return string(b)
}
func (f *textUnmarshalerFlag[T, P]) Type() string { return "string" }
func (f textUnmarshalerFlag) Set(v string) error {
return f.inner.UnmarshalText([]byte(v))
}
////////////////////////////////////////////////////////////////////////////////
func (f textUnmarshalerFlag) String() string {
return fmt.Sprint(f.inner)
}
type (
hostNameFlag = textUnmarshalerFlag[nebula.HostName, *nebula.HostName]
ipNetFlag = textUnmarshalerFlag[nebula.IPNet, *nebula.IPNet]
ipFlag = textUnmarshalerFlag[netip.Addr, *netip.Addr]
)
func (f textUnmarshalerFlag) Type() string { return "string" }

View File

@ -6,6 +6,8 @@ import (
"os"
"path/filepath"
"syscall"
"isle/daemon"
)
// minio-client keeps a configuration directory which contains various pieces of
@ -33,9 +35,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(ctx subCmdCtx) error {
do: func(subCmdCtx subCmdCtx) error {
flags := ctx.flagSet(true)
flags := subCmdCtx.flagSet(true)
keyID := flags.StringP(
"key-id", "i", "",
@ -47,11 +49,14 @@ var subCmdGarageMC = subCmd{
"Optional key secret to use, defaults to that of the shared global key",
)
if err := flags.Parse(ctx.args); err != nil {
if err := flags.Parse(subCmdCtx.args); err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
clientParams, err := ctx.daemonRPC.GetGarageClientParams(ctx)
var clientParams daemon.GarageClientParams
err := subCmdCtx.daemonRCPClient.Call(
subCmdCtx.ctx, &clientParams, "GetGarageClientParams", nil,
)
if err != nil {
return fmt.Errorf("calling GetGarageClientParams: %w", err)
}
@ -113,9 +118,12 @@ 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(ctx subCmdCtx) error {
do: func(subCmdCtx subCmdCtx) error {
clientParams, err := ctx.daemonRPC.GetGarageClientParams(ctx)
var clientParams daemon.GarageClientParams
err := subCmdCtx.daemonRCPClient.Call(
subCmdCtx.ctx, &clientParams, "GetGarageClientParams", nil,
)
if err != nil {
return fmt.Errorf("calling GetGarageClientParams: %w", err)
}
@ -126,7 +134,7 @@ var subCmdGarageCLI = subCmd{
var (
binPath = binPath("garage")
args = append([]string{"garage"}, ctx.args...)
args = append([]string{"garage"}, subCmdCtx.args...)
cliEnv = append(
os.Environ(),
"GARAGE_RPC_HOST="+clientParams.Peer.RPCPeerAddr(),
@ -148,8 +156,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(ctx subCmdCtx) error {
return ctx.doSubCmd(
do: func(subCmdCtx subCmdCtx) error {
return subCmdCtx.doSubCmd(
subCmdGarageCLI,
subCmdGarageMC,
)

View File

@ -7,27 +7,31 @@ import (
"isle/bootstrap"
"isle/daemon"
"isle/jsonutil"
"isle/nebula"
"net/netip"
"os"
"sort"
)
var subCmdHostCreate = subCmd{
var subCmdHostsCreate = subCmd{
name: "create",
descr: "Creates a new host in the network, writing its new bootstrap.json to stdout",
do: func(ctx subCmdCtx) error {
do: func(subCmdCtx subCmdCtx) error {
var (
flags = ctx.flagSet(false)
hostName hostNameFlag
ip ipFlag
flags = subCmdCtx.flagSet(false)
hostName nebula.HostName
ip netip.Addr
)
hostNameF := flags.VarPF(
&hostName,
"hostname", "n",
textUnmarshalerFlag{&hostName},
"hostname", "h",
"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.")
flags.VarP(
textUnmarshalerFlag{&ip}, "ip", "i", "IP of the new host",
)
canCreateHosts := flags.Bool(
"can-create-hosts",
@ -35,7 +39,7 @@ var subCmdHostCreate = subCmd{
"The new host should have the ability to create hosts too",
)
if err := flags.Parse(ctx.args); err != nil {
if err := flags.Parse(subCmdCtx.args); err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
@ -43,10 +47,17 @@ var subCmdHostCreate = subCmd{
return errors.New("--hostname is required")
}
res, err := ctx.daemonRPC.CreateHost(
ctx, hostName.V, daemon.CreateHostOpts{
IP: ip.V,
CanCreateHosts: *canCreateHosts,
var res daemon.CreateHostResult
err := subCmdCtx.daemonRCPClient.Call(
subCmdCtx.ctx,
&res,
"CreateHost",
daemon.CreateHostRequest{
HostName: hostName,
Opts: daemon.CreateHostOpts{
IP: ip,
CanCreateHosts: *canCreateHosts,
},
},
)
if err != nil {
@ -57,11 +68,11 @@ var subCmdHostCreate = subCmd{
},
}
var subCmdHostList = subCmd{
var subCmdHostsList = subCmd{
name: "list",
descr: "Lists all hosts in the network, and their IPs",
do: func(ctx subCmdCtx) error {
hostsRes, err := ctx.getHosts()
do: func(subCmdCtx subCmdCtx) error {
hostsRes, err := subCmdCtx.getHosts()
if err != nil {
return fmt.Errorf("calling GetHosts: %w", err)
}
@ -93,22 +104,22 @@ var subCmdHostList = subCmd{
},
}
var subCmdHostRemove = subCmd{
var subCmdHostsRemove = subCmd{
name: "remove",
descr: "Removes a host from the network",
do: func(ctx subCmdCtx) error {
do: func(subCmdCtx subCmdCtx) error {
var (
flags = ctx.flagSet(false)
hostName hostNameFlag
flags = subCmdCtx.flagSet(false)
hostName nebula.HostName
)
hostNameF := flags.VarPF(
&hostName,
"hostname", "n",
textUnmarshalerFlag{&hostName},
"hostname", "h",
"Name of the host to remove",
)
if err := flags.Parse(ctx.args); err != nil {
if err := flags.Parse(subCmdCtx.args); err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
@ -116,7 +127,11 @@ var subCmdHostRemove = subCmd{
return errors.New("--hostname is required")
}
_, err := ctx.daemonRPC.RemoveHost(ctx, hostName.V)
err := subCmdCtx.daemonRCPClient.Call(
subCmdCtx.ctx, nil, "RemoveHost", daemon.RemoveHostRequest{
HostName: hostName,
},
)
if err != nil {
return fmt.Errorf("calling RemoveHost: %w", err)
}
@ -125,15 +140,14 @@ var subCmdHostRemove = subCmd{
},
}
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,
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,
)
},
}

View File

@ -57,13 +57,13 @@ func main() {
}()
err := subCmdCtx{
Context: ctx,
args: os.Args[1:],
logger: logger,
args: os.Args[1:],
ctx: ctx,
logger: logger,
}.doSubCmd(
subCmdDaemon,
subCmdGarage,
subCmdHost,
subCmdHosts,
subCmdNebula,
subCmdNetwork,
subCmdVersion,

View File

@ -3,6 +3,7 @@ package main
import (
"errors"
"fmt"
"isle/daemon"
"isle/jsonutil"
"isle/nebula"
"os"
@ -11,15 +12,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(ctx subCmdCtx) error {
do: func(subCmdCtx subCmdCtx) error {
var (
flags = ctx.flagSet(false)
hostName hostNameFlag
flags = subCmdCtx.flagSet(false)
hostName nebula.HostName
)
hostNameF := flags.VarPF(
&hostName,
"hostname", "n",
textUnmarshalerFlag{&hostName},
"hostname", "h",
"Name of the host to generate a certificate for",
)
@ -28,7 +29,7 @@ var subCmdNebulaCreateCert = subCmd{
`Path to PEM file containing public key which will be embedded in the cert.`,
)
if err := flags.Parse(ctx.args); err != nil {
if err := flags.Parse(subCmdCtx.args); err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
@ -46,14 +47,21 @@ var subCmdNebulaCreateCert = subCmd{
return fmt.Errorf("unmarshaling public key as PEM: %w", err)
}
res, err := ctx.daemonRPC.CreateNebulaCertificate(
ctx, hostName.V, hostPub,
var res daemon.CreateNebulaCertificateResult
err = subCmdCtx.daemonRCPClient.Call(
subCmdCtx.ctx,
&res,
"CreateNebulaCertificate",
daemon.CreateNebulaCertificateRequest{
HostName: hostName,
HostEncryptingPublicKey: hostPub,
},
)
if err != nil {
return fmt.Errorf("calling CreateNebulaCertificate: %w", err)
}
nebulaHostCertPEM, err := res.HostNebulaCertificate.Unwrap().MarshalToPEM()
nebulaHostCertPEM, err := res.HostNebulaCertifcate.Unwrap().MarshalToPEM()
if err != nil {
return fmt.Errorf("marshaling cert to PEM: %w", err)
}
@ -69,19 +77,22 @@ var subCmdNebulaCreateCert = subCmd{
var subCmdNebulaShow = subCmd{
name: "show",
descr: "Writes nebula network information to stdout in JSON format",
do: func(ctx subCmdCtx) error {
do: func(subCmdCtx subCmdCtx) error {
flags := ctx.flagSet(false)
if err := flags.Parse(ctx.args); err != nil {
flags := subCmdCtx.flagSet(false)
if err := flags.Parse(subCmdCtx.args); err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
hosts, err := ctx.getHosts()
hosts, err := subCmdCtx.getHosts()
if err != nil {
return fmt.Errorf("getting hosts: %w", err)
}
caPublicCreds, err := ctx.daemonRPC.GetNebulaCAPublicCredentials(ctx)
var caPublicCreds nebula.CAPublicCredentials
err = subCmdCtx.daemonRCPClient.Call(
subCmdCtx.ctx, &caPublicCreds, "GetNebulaCAPublicCredentials", nil,
)
if err != nil {
return fmt.Errorf("calling GetNebulaCAPublicCredentials: %w", err)
}
@ -134,8 +145,8 @@ var subCmdNebulaShow = subCmd{
var subCmdNebula = subCmd{
name: "nebula",
descr: "Sub-commands related to the nebula VPN",
do: func(ctx subCmdCtx) error {
return ctx.doSubCmd(
do: func(subCmdCtx subCmdCtx) error {
return subCmdCtx.doSubCmd(
subCmdNebulaCreateCert,
subCmdNebulaShow,
)

View File

@ -10,50 +10,48 @@ import (
var subCmdNetworkCreate = subCmd{
name: "create",
descr: "Create's a new network, with this host being the first host in that network.",
do: func(ctx subCmdCtx) error {
do: func(subCmdCtx subCmdCtx) error {
var (
flags = ctx.flagSet(false)
ipNet ipNetFlag
hostName hostNameFlag
ctx = subCmdCtx.ctx
flags = subCmdCtx.flagSet(false)
req daemon.CreateNetworkRequest
)
name := flags.StringP(
"name", "N", "",
flags.StringVarP(
&req.Name, "name", "n", "",
"Human-readable name to identify the network as.",
)
domain := flags.StringP(
"domain", "d", "",
flags.StringVarP(
&req.Domain, "domain", "d", "",
"Domain name that should be used as the root domain in the network.",
)
ipNetF := flags.VarPF(
&ipNet, "ip-net", "i",
textUnmarshalerFlag{&req.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(
&hostName,
"hostname", "n",
textUnmarshalerFlag{&req.HostName},
"hostname", "h",
"Name of this host, which will be the first host in the network",
)
if err := flags.Parse(ctx.args); err != nil {
if err := flags.Parse(subCmdCtx.args); err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
if *name == "" ||
*domain == "" ||
if req.Name == "" ||
req.Domain == "" ||
!ipNetF.Changed ||
!hostNameF.Changed {
return errors.New("--name, --domain, --ip-net, and --hostname are required")
}
_, err := ctx.daemonRPC.CreateNetwork(
ctx, *name, *domain, ipNet.V, hostName.V,
)
err := subCmdCtx.daemonRCPClient.Call(ctx, nil, "CreateNetwork", req)
if err != nil {
return fmt.Errorf("creating network: %w", err)
}
@ -65,15 +63,17 @@ var subCmdNetworkCreate = subCmd{
var subCmdNetworkJoin = subCmd{
name: "join",
descr: "Joins this host to an existing network",
do: func(ctx subCmdCtx) error {
do: func(subCmdCtx subCmdCtx) error {
var (
flags = ctx.flagSet(false)
bootstrapPath = flags.StringP(
"bootstrap-path", "b", "", "Path to a bootstrap.json file.",
)
ctx = subCmdCtx.ctx
flags = subCmdCtx.flagSet(false)
)
if err := flags.Parse(ctx.args); err != nil {
bootstrapPath := flags.StringP(
"bootstrap-path", "b", "", "Path to a bootstrap.json file.",
)
if err := flags.Parse(subCmdCtx.args); err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
@ -88,16 +88,17 @@ var subCmdNetworkJoin = subCmd{
)
}
_, err := ctx.daemonRPC.JoinNetwork(ctx, newBootstrap)
return err
return subCmdCtx.daemonRCPClient.Call(
ctx, nil, "JoinNetwork", newBootstrap,
)
},
}
var subCmdNetwork = subCmd{
name: "network",
descr: "Sub-commands related to network membership",
do: func(ctx subCmdCtx) error {
return ctx.doSubCmd(
do: func(subCmdCtx subCmdCtx) error {
return subCmdCtx.doSubCmd(
subCmdNetworkCreate,
subCmdNetworkJoin,
)

View File

@ -12,41 +12,21 @@ 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
logger *mlog.Logger
daemonRPC daemon.RPC
ctx context.Context
logger *mlog.Logger
daemonRCPClient jsonrpc2.Client
}
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 {
@ -59,7 +39,7 @@ func (ctx subCmdCtx) usagePrefix() string {
return fmt.Sprintf("\nUSAGE: %s %s", os.Args[0], subCmdNamesStr)
}
func (ctx subCmdCtx) flagSet(withPassthrough bool) flagSet {
func (ctx subCmdCtx) flagSet(withPassthrough bool) *pflag.FlagSet {
flags := pflag.NewFlagSet(ctx.subCmd.name, pflag.ExitOnError)
flags.Usage = func() {
@ -78,7 +58,7 @@ func (ctx subCmdCtx) flagSet(withPassthrough bool) flagSet {
os.Stderr.Sync()
os.Exit(2)
}
return flagSet{flags}
return flags
}
func (ctx subCmdCtx) doSubCmd(subCmds ...subCmd) error {
@ -96,11 +76,7 @@ func (ctx subCmdCtx) doSubCmd(subCmds ...subCmd) error {
fmt.Fprintf(os.Stderr, "\nSUB-COMMANDS:\n\n")
for _, subCmd := range subCmds {
name := subCmd.name
if subCmd.plural != "" {
name += "(" + subCmd.plural + ")"
}
fmt.Fprintf(os.Stderr, " %s\t%s\n", name, subCmd.descr)
fmt.Fprintf(os.Stderr, " %s\t%s\n", subCmd.name, subCmd.descr)
}
fmt.Fprintf(os.Stderr, "\n")
@ -116,10 +92,8 @@ 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:]
@ -129,17 +103,17 @@ func (ctx subCmdCtx) doSubCmd(subCmds ...subCmd) error {
printUsageExit(subCmdName)
}
daemonRPC := daemon.RPCFromClient(
jsonrpc2.NewUnixHTTPClient(daemon.HTTPSocketPath(), daemonHTTPRPCPath),
daemonRCPClient := jsonrpc2.NewUnixHTTPClient(
daemon.HTTPSocketPath(), daemonHTTPRPCPath,
)
err := subCmd.do(subCmdCtx{
Context: ctx.Context,
subCmd: subCmd,
args: args,
subCmdNames: append(ctx.subCmdNames, subCmdName),
logger: ctx.logger,
daemonRPC: daemonRPC,
subCmd: subCmd,
args: args,
subCmdNames: append(ctx.subCmdNames, subCmdName),
ctx: ctx.ctx,
logger: ctx.logger,
daemonRCPClient: daemonRCPClient,
})
if err != nil {

View File

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

View File

@ -75,14 +75,6 @@ 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

View File

@ -1,108 +0,0 @@
// 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
}

View File

@ -32,8 +32,6 @@ 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
@ -87,9 +85,6 @@ 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(
@ -690,7 +685,6 @@ func (d *daemon) CreateHost(
)
}
}
// TODO if the ip is given, check that it's not already in use.
caSigningPrivateKey, err := getNebulaCASigningPrivateKey(
ctx, d.secretsStore,

View File

@ -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
}

View File

@ -1,32 +0,0 @@
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}}

View File

@ -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)

View File

@ -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 {

View File

@ -18,37 +18,29 @@ type methodDispatchFunc func(context.Context, Request) (any, error)
func newMethodDispatchFunc(
method reflect.Value,
) methodDispatchFunc {
paramTs := make([]reflect.Type, method.Type().NumIn()-1)
for i := range paramTs {
paramTs[i] = method.Type().In(i + 1)
}
paramT := method.Type().In(1)
return func(ctx context.Context, req Request) (any, error) {
callVals := make([]reflect.Value, 0, len(paramTs)+1)
callVals = append(callVals, reflect.ValueOf(ctx))
var (
ctxV = reflect.ValueOf(ctx)
paramPtrV = reflect.New(paramT)
)
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
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,
)
}
callVals = append(callVals, paramPtrV.Elem())
return nil, err
}
var (
callResV = method.Call(callVals)
callResV = method.Call([]reflect.Value{ctxV, paramPtrV.Elem()})
resV = callResV[0]
errV = callResV[1]
)
@ -94,7 +86,7 @@ func NewDispatchHandler(i any) Handler {
)
if !method.IsExported() ||
methodT.NumIn() < 1 ||
methodT.NumIn() != 2 ||
methodT.In(0) != ctxT ||
methodT.NumOut() != 2 ||
methodT.Out(1) != errT {

View File

@ -3,7 +3,6 @@ package jsonrpc2
import (
"context"
"errors"
"fmt"
"dev.mediocregopher.com/mediocre-go-lib.git/mctx"
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
@ -60,14 +59,9 @@ func NewMLogMiddleware(logger *mlog.Logger) Middleware {
)
if logger.MaxLevel() >= mlog.LevelDebug.Int() {
ctx := ctx
for i := range req.Params {
ctx = mctx.Annotate(
ctx,
fmt.Sprintf("rpcRequestParam%d", i),
string(req.Params[i]),
)
}
ctx := mctx.Annotate(
ctx, "rpcRequestParams", string(req.Params),
)
logger.Debug(ctx, "Handling RPC request")
}

View File

@ -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,19 +37,13 @@ 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,
) {
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)
}
paramsB, err := json.Marshal(params)
if err != nil {
return "", fmt.Errorf("encoding params as JSON: %w", err)
}
var (
@ -57,7 +51,7 @@ func encodeRequest(
reqEnvelope = Request{
Version: version,
Method: method,
Params: paramsBs,
Params: paramsB,
ID: id,
}
)

View File

@ -26,22 +26,14 @@ var ErrDivideByZero = Error{
type dividerImpl struct{}
func (dividerImpl) Divide2(ctx context.Context, top, bottom int) (int, error) {
if bottom == 0 {
func (dividerImpl) Divide(ctx context.Context, p DivideParams) (int, error) {
if p.Bottom == 0 {
return 0, ErrDivideByZero
}
if top%bottom != 0 {
if p.Top%p.Bottom != 0 {
return 0, errors.New("numbers don't divide evenly, cannot compute!")
}
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)
return p.Top / p.Bottom, nil
}
func (dividerImpl) Hidden(ctx context.Context, p struct{}) (int, error) {
@ -49,8 +41,6 @@ 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)
}
@ -92,26 +82,6 @@ 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) {

View File

@ -11,115 +11,49 @@ 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 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 {
type RPC struct {
daemon Daemon
}
// NewRPC initializes and returns an RPC instance.
func NewRPC(daemon Daemon) RPC {
return &rpcImpl{daemon}
func NewRPC(daemon Daemon) *RPC {
return &RPC{daemon}
}
func (r *rpcImpl) CreateNetwork(
ctx context.Context,
name string,
domain string,
ipNet nebula.IPNet,
hostName nebula.HostName,
// 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,
) (
struct{}, error,
) {
return struct{}{}, r.daemon.CreateNetwork(
ctx, name, domain, ipNet, hostName,
ctx, req.Name, req.Domain, req.IPNet, req.HostName,
)
}
func (r *rpcImpl) JoinNetwork(
// JoinNetwork passes through to the Daemon method of the same name.
func (r *RPC) JoinNetwork(
ctx context.Context, req JoiningBootstrap,
) (
struct{}, error,
@ -127,7 +61,17 @@ func (r *rpcImpl) JoinNetwork(
return struct{}{}, r.daemon.JoinNetwork(ctx, req)
}
func (r *rpcImpl) GetHosts(ctx context.Context) (GetHostsResult, error) {
// 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,
) {
b, err := r.daemon.GetBootstrap(ctx)
if err != nil {
return GetHostsResult{}, fmt.Errorf("retrieving bootstrap: %w", err)
@ -141,16 +85,19 @@ func (r *rpcImpl) GetHosts(ctx context.Context) (GetHostsResult, error) {
return GetHostsResult{hosts}, nil
}
func (r *rpcImpl) GetGarageClientParams(
ctx context.Context,
// GetGarageClientParams passes the call through to the Daemon method of the
// same name.
func (r *RPC) GetGarageClientParams(
ctx context.Context, req struct{},
) (
GarageClientParams, error,
) {
return r.daemon.GetGarageClientParams(ctx)
}
func (r *rpcImpl) GetNebulaCAPublicCredentials(
ctx context.Context,
// GetNebulaCAPublicCredentials returns the CAPublicCredentials for the network.
func (r *RPC) GetNebulaCAPublicCredentials(
ctx context.Context, req struct{},
) (
nebula.CAPublicCredentials, error,
) {
@ -164,20 +111,42 @@ func (r *rpcImpl) GetNebulaCAPublicCredentials(
return b.CAPublicCredentials, nil
}
func (r *rpcImpl) RemoveHost(
ctx context.Context, hostName nebula.HostName,
) (
struct{}, error,
) {
return struct{}{}, r.daemon.RemoveHost(ctx, hostName)
// RemoveHostRequest contains the arguments to the RemoveHost RPC method.
//
// All fields are required.
type RemoveHostRequest struct {
HostName nebula.HostName
}
func (r *rpcImpl) CreateHost(
ctx context.Context, hostName nebula.HostName, opts CreateHostOpts,
// 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,
) (
CreateHostResult, error,
) {
joiningBootstrap, err := r.daemon.CreateHost(ctx, hostName, opts)
joiningBootstrap, err := r.daemon.CreateHost(
ctx, req.HostName, req.Opts,
)
if err != nil {
return CreateHostResult{}, err
}
@ -185,19 +154,36 @@ func (r *rpcImpl) CreateHost(
return CreateHostResult{JoiningBootstrap: joiningBootstrap}, nil
}
func (r *rpcImpl) CreateNebulaCertificate(
ctx context.Context,
hostName nebula.HostName,
hostEncryptingPublicKey nebula.EncryptingPublicKey,
// 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,
) (
CreateNebulaCertificateResult, error,
) {
cert, err := r.daemon.CreateNebulaCertificate(
ctx, hostName, hostEncryptingPublicKey,
ctx, req.HostName, req.HostEncryptingPublicKey,
)
if err != nil {
return CreateNebulaCertificateResult{}, err
}
return CreateNebulaCertificateResult{HostNebulaCertificate: cert}, nil
return CreateNebulaCertificateResult{
HostNebulaCertifcate: cert,
}, nil
}

View File

@ -11,11 +11,6 @@ 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) {

View File

@ -1,17 +0,0 @@
{
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" ];
}

View File

@ -14,7 +14,6 @@ in pkgs.mkShell {
buildInputs = [
pkgs.go
pkgs.golangci-lint
(pkgs.callPackage ./nix/gowrap.nix {})
];
shellHook = ''
true # placeholder