Compare commits
No commits in common. "56f796e3fbb562ad1fe6c6d6bf8f5f2e8a15eb1b" and "cc121f0752a483ea9e0faaf640843a58e6c0ef3c" have entirely different histories.
56f796e3fb
...
cc121f0752
@ -173,7 +173,6 @@ in rec {
|
||||
pkgs.yq-go
|
||||
pkgs.jq
|
||||
pkgs.dig
|
||||
pkgs.nebula
|
||||
]}
|
||||
export SHELL=${pkgs.bash}/bin/bash
|
||||
exec ${pkgs.bash}/bin/bash ${./tests}/entrypoint.sh "$@"
|
||||
|
@ -29,7 +29,7 @@ To create a `bootstrap.json` file for the new host, the admin should perform the
|
||||
following command from their own host:
|
||||
|
||||
```
|
||||
isle hosts create \
|
||||
isle admin create-bootstrap \
|
||||
--hostname <name> \
|
||||
--ip <ip> \
|
||||
--admin-path <path to admin.json> \
|
||||
@ -48,12 +48,12 @@ The user can now proceed with calling `isle network join`, as described in the
|
||||
### Encrypted `admin.json`
|
||||
|
||||
If `admin.json` is kept in an encrypted format on disk (it should be!) then the
|
||||
decrypted form can be piped into `isle hosts create` over stdin. For example, if
|
||||
decrypted form can be piped into `create-bootstrap` over stdin. For example, if
|
||||
GPG is being used to secure `admin.json` then the following could be used to
|
||||
generate a `bootstrap.json`:
|
||||
|
||||
```
|
||||
gpg -d <path to admin.json.gpg> | isle hosts create \
|
||||
gpg -d <path to admin.json.gpg> | isle admin create-bootstrap \
|
||||
--hostname <name> \
|
||||
--ip <ip> \
|
||||
--admin-path - \
|
||||
|
@ -25,6 +25,7 @@ type Admin struct {
|
||||
}
|
||||
|
||||
Garage struct {
|
||||
RPCSecret string
|
||||
GlobalBucketS3APICredentials garage.S3APICredentials
|
||||
}
|
||||
}
|
||||
|
@ -30,6 +30,10 @@ func AppDirPath(appDirPath string) string {
|
||||
|
||||
// Garage contains parameters needed to connect to and use the garage cluster.
|
||||
type Garage struct {
|
||||
// TODO this should be part of some new configuration section related to
|
||||
// secrets which may or may not be granted to this host
|
||||
RPCSecret string
|
||||
|
||||
AdminToken string
|
||||
|
||||
// TODO this should be part of admin.CreationParams
|
||||
|
@ -4,6 +4,24 @@ import (
|
||||
"isle/garage"
|
||||
)
|
||||
|
||||
// GarageClientParams contains all the data needed to instantiate garage
|
||||
// clients.
|
||||
type GarageClientParams struct {
|
||||
Peer garage.RemotePeer
|
||||
GlobalBucketS3APICredentials garage.S3APICredentials
|
||||
RPCSecret string
|
||||
}
|
||||
|
||||
// GlobalBucketS3APIClient returns an S3 client pre-configured with access to
|
||||
// the global bucket.
|
||||
func (p GarageClientParams) GlobalBucketS3APIClient() garage.S3APIClient {
|
||||
var (
|
||||
addr = p.Peer.S3APIAddr()
|
||||
creds = p.GlobalBucketS3APICredentials
|
||||
)
|
||||
return garage.NewS3APIClient(addr, creds)
|
||||
}
|
||||
|
||||
// GaragePeers returns a Peer for each known garage instance in the network.
|
||||
func (b Bootstrap) GaragePeers() []garage.RemotePeer {
|
||||
|
||||
@ -51,3 +69,12 @@ func (b Bootstrap) ChooseGaragePeer() garage.RemotePeer {
|
||||
|
||||
panic("no garage instances configured")
|
||||
}
|
||||
|
||||
// GarageClientParams returns a GarageClientParams.
|
||||
func (b Bootstrap) GarageClientParams() GarageClientParams {
|
||||
return GarageClientParams{
|
||||
Peer: b.ChooseGaragePeer(),
|
||||
GlobalBucketS3APICredentials: b.Garage.GlobalBucketS3APICredentials,
|
||||
RPCSecret: b.Garage.RPCSecret,
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ package bootstrap
|
||||
import (
|
||||
"fmt"
|
||||
"isle/nebula"
|
||||
"net/netip"
|
||||
"net"
|
||||
)
|
||||
|
||||
// NebulaHost describes the nebula configuration of a Host which is relevant for
|
||||
@ -77,17 +77,10 @@ type Host struct {
|
||||
//
|
||||
// This assumes that the Host and its data has already been verified against the
|
||||
// CA signing key.
|
||||
func (h Host) IP() netip.Addr {
|
||||
func (h Host) IP() net.IP {
|
||||
cert := h.PublicCredentials.Cert.Unwrap()
|
||||
if len(cert.Details.Ips) == 0 {
|
||||
panic(fmt.Sprintf("host %q not configured with any ips: %+v", h.Name, h))
|
||||
}
|
||||
|
||||
ip := cert.Details.Ips[0].IP
|
||||
addr, ok := netip.AddrFromSlice(ip)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("ip %q (%#v) is not valid, somehow", ip, ip))
|
||||
}
|
||||
|
||||
return addr
|
||||
return cert.Details.Ips[0].IP
|
||||
}
|
||||
|
@ -1,11 +1,25 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"isle/admin"
|
||||
"isle/bootstrap"
|
||||
"isle/nebula"
|
||||
"net/netip"
|
||||
"os"
|
||||
)
|
||||
|
||||
func randStr(l int) string {
|
||||
b := make([]byte, l)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
func readAdmin(path string) (admin.Admin, error) {
|
||||
|
||||
if path == "-" {
|
||||
@ -26,3 +40,160 @@ func readAdmin(path string) (admin.Admin, error) {
|
||||
|
||||
return admin.FromReader(f)
|
||||
}
|
||||
|
||||
var subCmdAdminCreateBootstrap = subCmd{
|
||||
name: "create-bootstrap",
|
||||
descr: "Creates a new bootstrap.json file for a particular host and writes it to stdout",
|
||||
do: func(subCmdCtx subCmdCtx) error {
|
||||
var (
|
||||
flags = subCmdCtx.flagSet(false)
|
||||
hostName nebula.HostName
|
||||
ip netip.Addr
|
||||
)
|
||||
|
||||
hostNameF := flags.VarPF(
|
||||
textUnmarshalerFlag{&hostName},
|
||||
"hostname", "h",
|
||||
"Name of the host to generate bootstrap.json for",
|
||||
)
|
||||
|
||||
ipF := flags.VarPF(
|
||||
textUnmarshalerFlag{&ip}, "ip", "i", "IP of the new host",
|
||||
)
|
||||
|
||||
adminPath := flags.StringP(
|
||||
"admin-path", "a", "",
|
||||
`Path to admin.json file. If the given path is "-" then stdin is used.`,
|
||||
)
|
||||
|
||||
if err := flags.Parse(subCmdCtx.args); err != nil {
|
||||
return fmt.Errorf("parsing flags: %w", err)
|
||||
}
|
||||
|
||||
if !hostNameF.Changed ||
|
||||
!ipF.Changed ||
|
||||
*adminPath == "" {
|
||||
return errors.New("--hostname, --ip, and --admin-path are required")
|
||||
}
|
||||
|
||||
adm, err := readAdmin(*adminPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading admin.json with --admin-path of %q: %w", *adminPath, err)
|
||||
}
|
||||
|
||||
garageBootstrap := bootstrap.Garage{
|
||||
RPCSecret: adm.Garage.RPCSecret,
|
||||
AdminToken: randStr(32),
|
||||
GlobalBucketS3APICredentials: adm.Garage.GlobalBucketS3APICredentials,
|
||||
}
|
||||
|
||||
newHostBootstrap, err := bootstrap.New(
|
||||
adm.Nebula.CACredentials,
|
||||
adm.CreationParams,
|
||||
garageBootstrap,
|
||||
hostName,
|
||||
ip,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("initializing bootstrap data: %w", err)
|
||||
}
|
||||
|
||||
hostsRes, err := subCmdCtx.getHosts()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting hosts: %w", err)
|
||||
}
|
||||
|
||||
for _, host := range hostsRes.Hosts {
|
||||
newHostBootstrap.Hosts[host.Name] = host
|
||||
}
|
||||
|
||||
return newHostBootstrap.WriteTo(os.Stdout)
|
||||
},
|
||||
}
|
||||
|
||||
var subCmdAdminCreateNebulaCert = subCmd{
|
||||
name: "create-nebula-cert",
|
||||
descr: "Creates a signed nebula certificate file and writes it to stdout",
|
||||
do: func(subCmdCtx subCmdCtx) error {
|
||||
var (
|
||||
flags = subCmdCtx.flagSet(false)
|
||||
hostName nebula.HostName
|
||||
ip netip.Addr
|
||||
)
|
||||
|
||||
hostNameF := flags.VarPF(
|
||||
textUnmarshalerFlag{&hostName},
|
||||
"hostname", "h",
|
||||
"Name of the host to generate a certificate for",
|
||||
)
|
||||
|
||||
ipF := flags.VarPF(
|
||||
textUnmarshalerFlag{&ip}, "ip", "i", "IP of the new host",
|
||||
)
|
||||
|
||||
adminPath := flags.StringP(
|
||||
"admin-path", "a", "",
|
||||
`Path to admin.json file. If the given path is "-" then stdin is used.`,
|
||||
)
|
||||
|
||||
pubKeyPath := flags.StringP(
|
||||
"public-key-path", "p", "",
|
||||
`Path to PEM file containing public key which will be embedded in the cert.`,
|
||||
)
|
||||
|
||||
if err := flags.Parse(subCmdCtx.args); err != nil {
|
||||
return fmt.Errorf("parsing flags: %w", err)
|
||||
}
|
||||
|
||||
if !hostNameF.Changed ||
|
||||
!ipF.Changed ||
|
||||
*adminPath == "" ||
|
||||
*pubKeyPath == "" {
|
||||
return errors.New("--hostname, --ip, --admin-path, and --pub-key-path are required")
|
||||
}
|
||||
|
||||
adm, err := readAdmin(*adminPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading admin.json with --admin-path of %q: %w", *adminPath, err)
|
||||
}
|
||||
|
||||
hostPubPEM, err := os.ReadFile(*pubKeyPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading public key from %q: %w", *pubKeyPath, err)
|
||||
}
|
||||
|
||||
var hostPub nebula.EncryptingPublicKey
|
||||
if err := hostPub.UnmarshalNebulaPEM(hostPubPEM); err != nil {
|
||||
return fmt.Errorf("unmarshaling public key as PEM: %w", err)
|
||||
}
|
||||
|
||||
nebulaHostCert, err := nebula.NewHostCert(
|
||||
adm.Nebula.CACredentials, hostPub, hostName, ip,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating cert: %w", err)
|
||||
}
|
||||
|
||||
nebulaHostCertPEM, err := nebulaHostCert.Unwrap().MarshalToPEM()
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshaling cert to PEM: %w", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stdout.Write([]byte(nebulaHostCertPEM)); err != nil {
|
||||
return fmt.Errorf("writing to stdout: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var subCmdAdmin = subCmd{
|
||||
name: "admin",
|
||||
descr: "Sub-commands which only admins can run",
|
||||
do: func(subCmdCtx subCmdCtx) error {
|
||||
return subCmdCtx.doSubCmd(
|
||||
subCmdAdminCreateBootstrap,
|
||||
subCmdAdminCreateNebulaCert,
|
||||
)
|
||||
},
|
||||
}
|
||||
|
@ -1,13 +1,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"isle/daemon"
|
||||
"isle/bootstrap"
|
||||
)
|
||||
|
||||
// minio-client keeps a configuration directory which contains various pieces of
|
||||
@ -53,7 +52,7 @@ var subCmdGarageMC = subCmd{
|
||||
return fmt.Errorf("parsing flags: %w", err)
|
||||
}
|
||||
|
||||
var clientParams daemon.GarageClientParams
|
||||
var clientParams bootstrap.GarageClientParams
|
||||
err := subCmdCtx.daemonRCPClient.Call(
|
||||
subCmdCtx.ctx, &clientParams, "GetGarageClientParams", nil,
|
||||
)
|
||||
@ -120,7 +119,7 @@ var subCmdGarageCLI = subCmd{
|
||||
descr: "Runs the garage binary, automatically configured to point to the garage sub-process of a running isle daemon",
|
||||
do: func(subCmdCtx subCmdCtx) error {
|
||||
|
||||
var clientParams daemon.GarageClientParams
|
||||
var clientParams bootstrap.GarageClientParams
|
||||
err := subCmdCtx.daemonRCPClient.Call(
|
||||
subCmdCtx.ctx, &clientParams, "GetGarageClientParams", nil,
|
||||
)
|
||||
@ -128,10 +127,6 @@ var subCmdGarageCLI = subCmd{
|
||||
return fmt.Errorf("calling GetGarageClientParams: %w", err)
|
||||
}
|
||||
|
||||
if clientParams.RPCSecret == "" {
|
||||
return errors.New("this host does not have the garage RPC secret")
|
||||
}
|
||||
|
||||
var (
|
||||
binPath = binPath("garage")
|
||||
args = append([]string{"garage"}, subCmdCtx.args...)
|
||||
|
@ -7,70 +7,10 @@ import (
|
||||
"isle/daemon"
|
||||
"isle/jsonutil"
|
||||
"isle/nebula"
|
||||
"net/netip"
|
||||
"os"
|
||||
"sort"
|
||||
)
|
||||
|
||||
var subCmdHostsCreate = subCmd{
|
||||
name: "create",
|
||||
descr: "Creates a new host in the network, writing its new bootstrap.json to stdout",
|
||||
do: func(subCmdCtx subCmdCtx) error {
|
||||
var (
|
||||
flags = subCmdCtx.flagSet(false)
|
||||
hostName nebula.HostName
|
||||
ip netip.Addr
|
||||
)
|
||||
|
||||
hostNameF := flags.VarPF(
|
||||
textUnmarshalerFlag{&hostName},
|
||||
"hostname", "h",
|
||||
"Name of the host to generate bootstrap.json for",
|
||||
)
|
||||
|
||||
ipF := flags.VarPF(
|
||||
textUnmarshalerFlag{&ip}, "ip", "i", "IP of the new host",
|
||||
)
|
||||
|
||||
adminPath := flags.StringP(
|
||||
"admin-path", "a", "",
|
||||
`Path to admin.json file. If the given path is "-" then stdin is used.`,
|
||||
)
|
||||
|
||||
if err := flags.Parse(subCmdCtx.args); err != nil {
|
||||
return fmt.Errorf("parsing flags: %w", err)
|
||||
}
|
||||
|
||||
if !hostNameF.Changed ||
|
||||
!ipF.Changed ||
|
||||
*adminPath == "" {
|
||||
return errors.New("--hostname, --ip, and --admin-path are required")
|
||||
}
|
||||
|
||||
adm, err := readAdmin(*adminPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading admin.json with --admin-path of %q: %w", *adminPath, err)
|
||||
}
|
||||
|
||||
var res daemon.CreateHostResult
|
||||
err = subCmdCtx.daemonRCPClient.Call(
|
||||
subCmdCtx.ctx,
|
||||
&res,
|
||||
"CreateHost",
|
||||
daemon.CreateHostRequest{
|
||||
CASigningPrivateKey: adm.Nebula.CACredentials.SigningPrivateKey,
|
||||
HostName: hostName,
|
||||
IP: ip,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("calling CreateHost: %w", err)
|
||||
}
|
||||
|
||||
return res.HostBootstrap.WriteTo(os.Stdout)
|
||||
},
|
||||
}
|
||||
|
||||
var subCmdHostsList = subCmd{
|
||||
name: "list",
|
||||
descr: "Lists all hosts in the network, and their IPs",
|
||||
@ -148,7 +88,6 @@ var subCmdHosts = subCmd{
|
||||
descr: "Sub-commands having to do with configuration of hosts in the network",
|
||||
do: func(subCmdCtx subCmdCtx) error {
|
||||
return subCmdCtx.doSubCmd(
|
||||
subCmdHostsCreate,
|
||||
subCmdHostsRemove,
|
||||
subCmdHostsList,
|
||||
)
|
||||
|
@ -61,6 +61,7 @@ func main() {
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
}.doSubCmd(
|
||||
subCmdAdmin,
|
||||
subCmdDaemon,
|
||||
subCmdGarage,
|
||||
subCmdHosts,
|
||||
|
@ -1,92 +1,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"isle/daemon"
|
||||
"isle/jsonutil"
|
||||
"isle/nebula"
|
||||
"os"
|
||||
)
|
||||
|
||||
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 {
|
||||
var (
|
||||
flags = subCmdCtx.flagSet(false)
|
||||
hostName nebula.HostName
|
||||
)
|
||||
|
||||
hostNameF := flags.VarPF(
|
||||
textUnmarshalerFlag{&hostName},
|
||||
"hostname", "h",
|
||||
"Name of the host to generate a certificate for",
|
||||
)
|
||||
|
||||
adminPath := flags.StringP(
|
||||
"admin-path", "a", "",
|
||||
`Path to admin.json file. If the given path is "-" then stdin is used.`,
|
||||
)
|
||||
|
||||
pubKeyPath := flags.StringP(
|
||||
"public-key-path", "p", "",
|
||||
`Path to PEM file containing public key which will be embedded in the cert.`,
|
||||
)
|
||||
|
||||
if err := flags.Parse(subCmdCtx.args); err != nil {
|
||||
return fmt.Errorf("parsing flags: %w", err)
|
||||
}
|
||||
|
||||
if !hostNameF.Changed ||
|
||||
*adminPath == "" ||
|
||||
*pubKeyPath == "" {
|
||||
return errors.New("--hostname, --admin-path, and --pub-key-path are required")
|
||||
}
|
||||
|
||||
adm, err := readAdmin(*adminPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading admin.json with --admin-path of %q: %w", *adminPath, err)
|
||||
}
|
||||
|
||||
hostPubPEM, err := os.ReadFile(*pubKeyPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading public key from %q: %w", *pubKeyPath, err)
|
||||
}
|
||||
|
||||
var hostPub nebula.EncryptingPublicKey
|
||||
if err := hostPub.UnmarshalNebulaPEM(hostPubPEM); err != nil {
|
||||
return fmt.Errorf("unmarshaling public key as PEM: %w", err)
|
||||
}
|
||||
|
||||
var res daemon.CreateNebulaCertificateResult
|
||||
err = subCmdCtx.daemonRCPClient.Call(
|
||||
subCmdCtx.ctx,
|
||||
&res,
|
||||
"CreateNebulaCertificate",
|
||||
daemon.CreateNebulaCertificateRequest{
|
||||
CASigningPrivateKey: adm.Nebula.CACredentials.SigningPrivateKey,
|
||||
HostName: hostName,
|
||||
HostEncryptingPublicKey: hostPub,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("calling CreateNebulaCertificate: %w", err)
|
||||
}
|
||||
|
||||
nebulaHostCertPEM, err := res.HostNebulaCertifcate.Unwrap().MarshalToPEM()
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshaling cert to PEM: %w", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stdout.Write([]byte(nebulaHostCertPEM)); err != nil {
|
||||
return fmt.Errorf("writing to stdout: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var subCmdNebulaShow = subCmd{
|
||||
name: "show",
|
||||
descr: "Writes nebula network information to stdout in JSON format",
|
||||
@ -110,17 +30,20 @@ var subCmdNebulaShow = subCmd{
|
||||
return fmt.Errorf("calling GetNebulaCAPublicCredentials: %w", err)
|
||||
}
|
||||
|
||||
caCert := caPublicCreds.Cert
|
||||
caCertDetails := caCert.Unwrap().Details
|
||||
caCert := caPublicCreds.Cert.Unwrap()
|
||||
caCertPEM, err := caCert.MarshalToPEM()
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshaling CA cert to PEM: %w", err)
|
||||
}
|
||||
|
||||
if len(caCertDetails.Subnets) != 1 {
|
||||
if len(caCert.Details.Subnets) != 1 {
|
||||
return fmt.Errorf(
|
||||
"malformed ca.crt, contains unexpected subnets %#v",
|
||||
caCertDetails.Subnets,
|
||||
caCert.Details.Subnets,
|
||||
)
|
||||
}
|
||||
|
||||
subnet := caCertDetails.Subnets[0]
|
||||
subnet := caCert.Details.Subnets[0]
|
||||
|
||||
type outLighthouse struct {
|
||||
PublicAddr string
|
||||
@ -128,11 +51,11 @@ var subCmdNebulaShow = subCmd{
|
||||
}
|
||||
|
||||
out := struct {
|
||||
CACert nebula.Certificate
|
||||
CACert string
|
||||
SubnetCIDR string
|
||||
Lighthouses []outLighthouse
|
||||
}{
|
||||
CACert: caCert,
|
||||
CACert: string(caCertPEM),
|
||||
SubnetCIDR: subnet.String(),
|
||||
}
|
||||
|
||||
@ -160,7 +83,6 @@ var subCmdNebula = subCmd{
|
||||
descr: "Sub-commands related to the nebula VPN",
|
||||
do: func(subCmdCtx subCmdCtx) error {
|
||||
return subCmdCtx.doSubCmd(
|
||||
subCmdNebulaCreateCert,
|
||||
subCmdNebulaShow,
|
||||
)
|
||||
},
|
||||
|
@ -101,7 +101,7 @@ func bootstrapGarageHostForAlloc(
|
||||
}
|
||||
|
||||
func garageWriteChildConfig(
|
||||
rpcSecret, runtimeDirPath string,
|
||||
runtimeDirPath string,
|
||||
hostBootstrap bootstrap.Bootstrap,
|
||||
alloc ConfigStorageAllocation,
|
||||
) (
|
||||
@ -129,7 +129,7 @@ func garageWriteChildConfig(
|
||||
MetaPath: alloc.MetaPath,
|
||||
DataPath: alloc.DataPath,
|
||||
|
||||
RPCSecret: rpcSecret,
|
||||
RPCSecret: hostBootstrap.Garage.RPCSecret,
|
||||
AdminToken: hostBootstrap.Garage.AdminToken,
|
||||
|
||||
LocalPeer: peer,
|
||||
@ -144,28 +144,20 @@ func garageWriteChildConfig(
|
||||
}
|
||||
|
||||
func garagePmuxProcConfigs(
|
||||
ctx context.Context,
|
||||
logger *mlog.Logger,
|
||||
rpcSecret, runtimeDirPath, binDirPath string,
|
||||
runtimeDirPath, binDirPath string,
|
||||
daemonConfig Config,
|
||||
hostBootstrap bootstrap.Bootstrap,
|
||||
) (
|
||||
[]pmuxlib.ProcessConfig, error,
|
||||
) {
|
||||
var (
|
||||
pmuxProcConfigs []pmuxlib.ProcessConfig
|
||||
allocs = daemonConfig.Storage.Allocations
|
||||
)
|
||||
|
||||
if len(allocs) > 0 && rpcSecret == "" {
|
||||
logger.WarnString(ctx, "Not starting garage instances for storage allocations, missing garage RPC secret")
|
||||
return nil, nil
|
||||
}
|
||||
var pmuxProcConfigs []pmuxlib.ProcessConfig
|
||||
|
||||
for _, alloc := range allocs {
|
||||
for _, alloc := range daemonConfig.Storage.Allocations {
|
||||
|
||||
childConfigPath, err := garageWriteChildConfig(
|
||||
rpcSecret, runtimeDirPath, hostBootstrap, alloc,
|
||||
runtimeDirPath, hostBootstrap, alloc,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("writing child config file for alloc %+v: %w", alloc, err)
|
||||
@ -184,7 +176,9 @@ func garagePmuxProcConfigs(
|
||||
return pmuxProcConfigs, nil
|
||||
}
|
||||
|
||||
func garageApplyLayout(
|
||||
// TODO don't expose this publicly once cluster creation is done via Daemon
|
||||
// interface.
|
||||
func GarageApplyLayout(
|
||||
ctx context.Context,
|
||||
logger *mlog.Logger,
|
||||
daemonConfig Config,
|
||||
|
@ -22,7 +22,7 @@ func waitForNebula(
|
||||
ctx context.Context, logger *mlog.Logger, hostBootstrap bootstrap.Bootstrap,
|
||||
) error {
|
||||
var (
|
||||
ip = net.IP(hostBootstrap.ThisHost().IP().AsSlice())
|
||||
ip = hostBootstrap.ThisHost().IP()
|
||||
lUDPAddr = &net.UDPAddr{IP: ip, Port: 0}
|
||||
rUDPAddr = &net.UDPAddr{IP: ip, Port: 45535}
|
||||
)
|
||||
|
@ -9,13 +9,8 @@ import (
|
||||
)
|
||||
|
||||
func (c *Children) newPmuxConfig(
|
||||
ctx context.Context,
|
||||
garageRPCSecret, binDirPath string,
|
||||
daemonConfig Config,
|
||||
hostBootstrap bootstrap.Bootstrap,
|
||||
) (
|
||||
pmuxlib.Config, error,
|
||||
) {
|
||||
binDirPath string, daemonConfig Config, hostBootstrap bootstrap.Bootstrap,
|
||||
) (pmuxlib.Config, error) {
|
||||
nebulaPmuxProcConfig, err := nebulaPmuxProcConfig(
|
||||
c.opts.EnvVars.RuntimeDirPath,
|
||||
binDirPath,
|
||||
@ -39,9 +34,7 @@ func (c *Children) newPmuxConfig(
|
||||
}
|
||||
|
||||
garagePmuxProcConfigs, err := garagePmuxProcConfigs(
|
||||
ctx,
|
||||
c.logger,
|
||||
garageRPCSecret,
|
||||
c.opts.EnvVars.RuntimeDirPath,
|
||||
binDirPath,
|
||||
daemonConfig,
|
||||
|
@ -2,11 +2,8 @@ package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"isle/bootstrap"
|
||||
"isle/garage"
|
||||
"isle/secrets"
|
||||
|
||||
"code.betamike.com/micropelago/pmux/pmuxlib"
|
||||
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
|
||||
@ -30,22 +27,15 @@ type Children struct {
|
||||
func NewChildren(
|
||||
ctx context.Context,
|
||||
logger *mlog.Logger,
|
||||
binDirPath string,
|
||||
secretsStore secrets.Store,
|
||||
daemonConfig Config,
|
||||
hostBootstrap bootstrap.Bootstrap,
|
||||
binDirPath string,
|
||||
opts *Opts,
|
||||
) (
|
||||
*Children, error,
|
||||
) {
|
||||
opts = opts.withDefaults()
|
||||
|
||||
logger.Info(ctx, "Loading secrets")
|
||||
garageRPCSecret, err := garage.GetRPCSecret(ctx, secretsStore)
|
||||
if err != nil && !errors.Is(err, secrets.ErrNotFound) {
|
||||
return nil, fmt.Errorf("loading garage RPC secret: %w", err)
|
||||
}
|
||||
|
||||
pmuxCtx, pmuxCancelFn := context.WithCancel(context.Background())
|
||||
|
||||
c := &Children{
|
||||
@ -55,9 +45,7 @@ func NewChildren(
|
||||
pmuxStoppedCh: make(chan struct{}),
|
||||
}
|
||||
|
||||
pmuxConfig, err := c.newPmuxConfig(
|
||||
ctx, garageRPCSecret, binDirPath, daemonConfig, hostBootstrap,
|
||||
)
|
||||
pmuxConfig, err := c.newPmuxConfig(binDirPath, daemonConfig, hostBootstrap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("generating pmux config: %w", err)
|
||||
}
|
||||
|
@ -13,10 +13,7 @@ import (
|
||||
"isle/bootstrap"
|
||||
"isle/garage"
|
||||
"isle/nebula"
|
||||
"isle/secrets"
|
||||
"net/netip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@ -55,39 +52,9 @@ type Daemon interface {
|
||||
// GetBootstraps returns the currently active Bootstrap.
|
||||
GetBootstrap(context.Context) (bootstrap.Bootstrap, error)
|
||||
|
||||
// GetGarageClientParams returns a GarageClientParams for the current
|
||||
// network state.
|
||||
GetGarageClientParams(context.Context) (GarageClientParams, error)
|
||||
|
||||
// RemoveHost removes the host of the given name from the network.
|
||||
RemoveHost(context.Context, nebula.HostName) error
|
||||
|
||||
// CreateHost creates a bootstrap for a new host with the given name and IP
|
||||
// address.
|
||||
CreateHost(
|
||||
ctx context.Context,
|
||||
caSigningPrivateKey nebula.SigningPrivateKey, // TODO load from secrets storage
|
||||
hostName nebula.HostName,
|
||||
ip netip.Addr, // TODO automatically choose IP address
|
||||
) (
|
||||
bootstrap.Bootstrap, error,
|
||||
)
|
||||
|
||||
// CreateNebulaCertificate creates and signs a new nebula certficate for an
|
||||
// existing host, given the public key for that host. This is currently
|
||||
// mostly useful for creating certs for mobile devices.
|
||||
//
|
||||
// Errors:
|
||||
// - ErrHostNotFound
|
||||
CreateNebulaCertificate(
|
||||
ctx context.Context,
|
||||
caSigningPrivateKey nebula.SigningPrivateKey, // TODO load from secrets storage
|
||||
hostName nebula.HostName,
|
||||
hostPubKey nebula.EncryptingPublicKey,
|
||||
) (
|
||||
nebula.Certificate, error,
|
||||
)
|
||||
|
||||
// Shutdown blocks until all resources held or created by the daemon,
|
||||
// including child processes it has started, have been cleaned up.
|
||||
//
|
||||
@ -140,8 +107,6 @@ type daemon struct {
|
||||
envBinDirPath string
|
||||
opts *Opts
|
||||
|
||||
secretsStore secrets.Store
|
||||
|
||||
l sync.RWMutex
|
||||
state int
|
||||
children *Children
|
||||
@ -189,17 +154,6 @@ func NewDaemon(
|
||||
return nil, fmt.Errorf("initializing daemon directories: %w", err)
|
||||
}
|
||||
|
||||
var (
|
||||
secretsPath = filepath.Join(d.opts.EnvVars.StateDirPath, "secrets")
|
||||
err error
|
||||
)
|
||||
|
||||
if d.secretsStore, err = secrets.NewFSStore(secretsPath); err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"initializing secrets store at %q: %w", secretsPath, err,
|
||||
)
|
||||
}
|
||||
|
||||
currBootstrap, err := bootstrap.FromFile(bootstrapFilePath)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
// daemon has never had a network created or joined
|
||||
@ -295,7 +249,7 @@ func (d *daemon) checkBootstrap(
|
||||
|
||||
thisHost := hostBootstrap.ThisHost()
|
||||
|
||||
newHosts, err := d.getGarageBootstrapHosts(ctx, d.logger, hostBootstrap)
|
||||
newHosts, err := getGarageBootstrapHosts(ctx, d.logger, hostBootstrap)
|
||||
if err != nil {
|
||||
return bootstrap.Bootstrap{}, false, fmt.Errorf("getting hosts from garage: %w", err)
|
||||
}
|
||||
@ -367,7 +321,7 @@ func (d *daemon) postInit(ctx context.Context) bool {
|
||||
d.logger,
|
||||
"Applying garage layout",
|
||||
func(ctx context.Context) error {
|
||||
return garageApplyLayout(
|
||||
return GarageApplyLayout(
|
||||
ctx, d.logger, d.daemonConfig, d.currBootstrap,
|
||||
)
|
||||
},
|
||||
@ -421,7 +375,7 @@ func (d *daemon) postInit(ctx context.Context) bool {
|
||||
d.logger,
|
||||
"Updating host info in garage",
|
||||
func(ctx context.Context) error {
|
||||
return d.putGarageBoostrapHost(ctx, d.logger, d.currBootstrap)
|
||||
return putGarageBoostrapHost(ctx, d.logger, d.currBootstrap)
|
||||
},
|
||||
) {
|
||||
return false
|
||||
@ -456,16 +410,15 @@ func (d *daemon) restartLoop(ctx context.Context, readyCh chan<- struct{}) {
|
||||
children, err := NewChildren(
|
||||
ctx,
|
||||
d.logger.WithNamespace("children"),
|
||||
d.envBinDirPath,
|
||||
d.secretsStore,
|
||||
d.daemonConfig,
|
||||
d.currBootstrap,
|
||||
d.envBinDirPath,
|
||||
d.opts,
|
||||
)
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return
|
||||
} else if err != nil {
|
||||
d.logger.Error(ctx, "failed to initialize child processes", err)
|
||||
d.logger.Error(ctx, "failed to initialize daemon", err)
|
||||
if !wait(1 * time.Second) {
|
||||
return
|
||||
}
|
||||
@ -522,25 +475,17 @@ func (d *daemon) CreateNetwork(
|
||||
return admin.Admin{}, fmt.Errorf("creating nebula CA cert: %w", err)
|
||||
}
|
||||
|
||||
var (
|
||||
adm = admin.Admin{
|
||||
CreationParams: admin.CreationParams{
|
||||
ID: randStr(32),
|
||||
Name: name,
|
||||
Domain: domain,
|
||||
},
|
||||
}
|
||||
adm := admin.Admin{
|
||||
CreationParams: admin.CreationParams{
|
||||
ID: randStr(32),
|
||||
Name: name,
|
||||
Domain: domain,
|
||||
},
|
||||
}
|
||||
|
||||
garageBootstrap = bootstrap.Garage{
|
||||
AdminToken: randStr(32),
|
||||
}
|
||||
)
|
||||
|
||||
garageRPCSecret := randStr(32)
|
||||
|
||||
err = garage.SetRPCSecret(ctx, d.secretsStore, garageRPCSecret)
|
||||
if err != nil {
|
||||
return admin.Admin{}, fmt.Errorf("storing garage RPC secret: %w", err)
|
||||
garageBootstrap := bootstrap.Garage{
|
||||
RPCSecret: randStr(32),
|
||||
AdminToken: randStr(32),
|
||||
}
|
||||
|
||||
hostBootstrap, err := bootstrap.New(
|
||||
@ -591,6 +536,7 @@ func (d *daemon) CreateNetwork(
|
||||
d.l.RUnlock()
|
||||
|
||||
adm.Nebula.CACredentials = nebulaCACreds
|
||||
adm.Garage.RPCSecret = hostBootstrap.Garage.RPCSecret
|
||||
adm.Garage.GlobalBucketS3APICredentials = garageGlobalBucketCreds
|
||||
|
||||
return adm, nil
|
||||
@ -632,20 +578,6 @@ func (d *daemon) GetBootstrap(ctx context.Context) (bootstrap.Bootstrap, error)
|
||||
})
|
||||
}
|
||||
|
||||
func (d *daemon) GetGarageClientParams(
|
||||
ctx context.Context,
|
||||
) (
|
||||
GarageClientParams, error,
|
||||
) {
|
||||
return withCurrBootstrap(d, func(
|
||||
currBootstrap bootstrap.Bootstrap,
|
||||
) (
|
||||
GarageClientParams, error,
|
||||
) {
|
||||
return d.getGarageClientParams(ctx, currBootstrap)
|
||||
})
|
||||
}
|
||||
|
||||
func (d *daemon) RemoveHost(ctx context.Context, hostName nebula.HostName) error {
|
||||
// TODO RemoveHost should publish a certificate revocation for the host
|
||||
// being removed.
|
||||
@ -654,93 +586,12 @@ func (d *daemon) RemoveHost(ctx context.Context, hostName nebula.HostName) error
|
||||
) (
|
||||
struct{}, error,
|
||||
) {
|
||||
garageClientParams, err := d.getGarageClientParams(ctx, currBootstrap)
|
||||
if err != nil {
|
||||
return struct{}{}, fmt.Errorf("get garage client params: %w", err)
|
||||
}
|
||||
|
||||
client := garageClientParams.GlobalBucketS3APIClient()
|
||||
client := currBootstrap.GarageClientParams().GlobalBucketS3APIClient()
|
||||
return struct{}{}, removeGarageBootstrapHost(ctx, client, hostName)
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func makeCACreds(
|
||||
currBootstrap bootstrap.Bootstrap,
|
||||
caSigningPrivateKey nebula.SigningPrivateKey,
|
||||
) nebula.CACredentials {
|
||||
return nebula.CACredentials{
|
||||
Public: currBootstrap.CAPublicCredentials,
|
||||
SigningPrivateKey: caSigningPrivateKey,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *daemon) CreateHost(
|
||||
ctx context.Context,
|
||||
caSigningPrivateKey nebula.SigningPrivateKey,
|
||||
hostName nebula.HostName,
|
||||
ip netip.Addr,
|
||||
) (
|
||||
bootstrap.Bootstrap, error,
|
||||
) {
|
||||
return withCurrBootstrap(d, func(
|
||||
currBootstrap bootstrap.Bootstrap,
|
||||
) (
|
||||
bootstrap.Bootstrap, error,
|
||||
) {
|
||||
garageGlobalBucketS3APICreds := currBootstrap.Garage.GlobalBucketS3APICredentials
|
||||
|
||||
garageBootstrap := bootstrap.Garage{
|
||||
AdminToken: randStr(32),
|
||||
GlobalBucketS3APICredentials: garageGlobalBucketS3APICreds,
|
||||
}
|
||||
|
||||
newHostBootstrap, err := bootstrap.New(
|
||||
makeCACreds(currBootstrap, caSigningPrivateKey),
|
||||
currBootstrap.AdminCreationParams,
|
||||
garageBootstrap,
|
||||
hostName,
|
||||
ip,
|
||||
)
|
||||
if err != nil {
|
||||
return bootstrap.Bootstrap{}, fmt.Errorf(
|
||||
"initializing bootstrap data: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
newHostBootstrap.Hosts = currBootstrap.Hosts
|
||||
|
||||
// TODO persist new bootstrap to garage. Requires making the daemon
|
||||
// config change watching logic smarter, so only dnsmasq gets restarted.
|
||||
|
||||
return newHostBootstrap, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (d *daemon) CreateNebulaCertificate(
|
||||
ctx context.Context,
|
||||
caSigningPrivateKey nebula.SigningPrivateKey,
|
||||
hostName nebula.HostName,
|
||||
hostPubKey nebula.EncryptingPublicKey,
|
||||
) (
|
||||
nebula.Certificate, error,
|
||||
) {
|
||||
return withCurrBootstrap(d, func(
|
||||
currBootstrap bootstrap.Bootstrap,
|
||||
) (
|
||||
nebula.Certificate, error,
|
||||
) {
|
||||
host, ok := currBootstrap.Hosts[hostName]
|
||||
if !ok {
|
||||
return nebula.Certificate{}, ErrHostNotFound
|
||||
}
|
||||
|
||||
caCreds := makeCACreds(currBootstrap, caSigningPrivateKey)
|
||||
|
||||
return nebula.NewHostCert(caCreds, hostPubKey, hostName, host.IP())
|
||||
})
|
||||
}
|
||||
|
||||
func (d *daemon) Shutdown() error {
|
||||
d.l.Lock()
|
||||
defer d.l.Unlock()
|
||||
|
@ -22,6 +22,34 @@ type EnvVars struct {
|
||||
|
||||
func (e EnvVars) init() error {
|
||||
var errs []error
|
||||
mkDir := func(path string) error {
|
||||
{
|
||||
parentPath := filepath.Dir(path)
|
||||
parentInfo, err := os.Stat(parentPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking parent path %q: %w", parentPath, err)
|
||||
} else if !parentInfo.IsDir() {
|
||||
return fmt.Errorf("%q is not a directory", parentPath)
|
||||
}
|
||||
}
|
||||
|
||||
info, err := os.Stat(path)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
// fine
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("checking path: %w", err)
|
||||
} else if !info.IsDir() {
|
||||
return fmt.Errorf("path is not a directory")
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := os.Mkdir(path, 0700); err != nil {
|
||||
return fmt.Errorf("creating directory: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := mkDir(e.RuntimeDirPath); err != nil {
|
||||
errs = append(errs, fmt.Errorf(
|
||||
"creating runtime directory %q: %w",
|
||||
|
@ -24,8 +24,4 @@ var (
|
||||
//
|
||||
// The Data field will be a string containing further details.
|
||||
ErrInvalidConfig = jsonrpc2.NewError(5, "Invalid daemon config")
|
||||
|
||||
// ErrHostNotFound is returned when performing an operation which expected a
|
||||
// host to exist in the network, but that host wasn't found.
|
||||
ErrHostNotFound = jsonrpc2.NewError(6, "Host not found")
|
||||
)
|
||||
|
@ -1,47 +0,0 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"isle/bootstrap"
|
||||
"isle/garage"
|
||||
"isle/secrets"
|
||||
)
|
||||
|
||||
// GarageClientParams contains all the data needed to instantiate garage
|
||||
// clients.
|
||||
type GarageClientParams struct {
|
||||
Peer garage.RemotePeer
|
||||
GlobalBucketS3APICredentials garage.S3APICredentials
|
||||
|
||||
// RPCSecret may be empty, if the secret is not available on the host.
|
||||
RPCSecret string
|
||||
}
|
||||
|
||||
func (d *daemon) getGarageClientParams(
|
||||
ctx context.Context, currBootstrap bootstrap.Bootstrap,
|
||||
) (
|
||||
GarageClientParams, error,
|
||||
) {
|
||||
rpcSecret, err := garage.GetRPCSecret(ctx, d.secretsStore)
|
||||
if err != nil && !errors.Is(err, secrets.ErrNotFound) {
|
||||
return GarageClientParams{}, fmt.Errorf("getting garage rpc secret: %w", err)
|
||||
}
|
||||
|
||||
return GarageClientParams{
|
||||
Peer: currBootstrap.ChooseGaragePeer(),
|
||||
GlobalBucketS3APICredentials: currBootstrap.Garage.GlobalBucketS3APICredentials,
|
||||
RPCSecret: rpcSecret,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GlobalBucketS3APIClient returns an S3 client pre-configured with access to
|
||||
// the global bucket.
|
||||
func (p GarageClientParams) GlobalBucketS3APIClient() garage.S3APIClient {
|
||||
var (
|
||||
addr = p.Peer.S3APIAddr()
|
||||
creds = p.GlobalBucketS3APICredentials
|
||||
)
|
||||
return garage.NewS3APIClient(addr, creds)
|
||||
}
|
@ -62,28 +62,24 @@ func garageInitializeGlobalBucket(
|
||||
// putGarageBoostrapHost places the <hostname>.json.signed file for this host
|
||||
// into garage so that other hosts are able to see relevant configuration for
|
||||
// it.
|
||||
func (d *daemon) putGarageBoostrapHost(
|
||||
func putGarageBoostrapHost(
|
||||
ctx context.Context, logger *mlog.Logger, currBootstrap bootstrap.Bootstrap,
|
||||
) error {
|
||||
garageClientParams, err := d.getGarageClientParams(ctx, currBootstrap)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting garage client params: %w", err)
|
||||
}
|
||||
|
||||
var (
|
||||
host = currBootstrap.ThisHost()
|
||||
client = garageClientParams.GlobalBucketS3APIClient()
|
||||
b = currBootstrap
|
||||
host = b.ThisHost()
|
||||
client = b.GarageClientParams().GlobalBucketS3APIClient()
|
||||
)
|
||||
|
||||
configured, err := nebula.Sign(
|
||||
host.HostConfigured, currBootstrap.PrivateCredentials.SigningPrivateKey,
|
||||
host.HostConfigured, b.PrivateCredentials.SigningPrivateKey,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("signing host configured data: %w", err)
|
||||
}
|
||||
|
||||
hostB, err := json.Marshal(bootstrap.AuthenticatedHost{
|
||||
Assigned: currBootstrap.SignedHostAssigned,
|
||||
Assigned: b.SignedHostAssigned,
|
||||
Configured: configured,
|
||||
})
|
||||
if err != nil {
|
||||
@ -111,18 +107,14 @@ func (d *daemon) putGarageBoostrapHost(
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *daemon) getGarageBootstrapHosts(
|
||||
func getGarageBootstrapHosts(
|
||||
ctx context.Context, logger *mlog.Logger, currBootstrap bootstrap.Bootstrap,
|
||||
) (
|
||||
map[nebula.HostName]bootstrap.Host, error,
|
||||
) {
|
||||
garageClientParams, err := d.getGarageClientParams(ctx, currBootstrap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting garage client params: %w", err)
|
||||
}
|
||||
|
||||
var (
|
||||
client = garageClientParams.GlobalBucketS3APIClient()
|
||||
b = currBootstrap
|
||||
client = b.GarageClientParams().GlobalBucketS3APIClient()
|
||||
hosts = map[nebula.HostName]bootstrap.Host{}
|
||||
|
||||
objInfoCh = client.ListObjects(
|
||||
@ -160,7 +152,7 @@ func (d *daemon) getGarageBootstrapHosts(
|
||||
continue
|
||||
}
|
||||
|
||||
host, err := authedHost.Unwrap(currBootstrap.CAPublicCredentials)
|
||||
host, err := authedHost.Unwrap(b.CAPublicCredentials)
|
||||
if err != nil {
|
||||
logger.Warn(ctx, "Host could not be authenticated", err)
|
||||
}
|
||||
|
@ -4,11 +4,6 @@ import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
|
||||
@ -43,34 +38,3 @@ func randStr(l int) string {
|
||||
}
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
// mkDir is like os.Mkdir but it returns better error messages. If the directory
|
||||
// already exists then nil is returned.
|
||||
func mkDir(path string) error {
|
||||
{
|
||||
parentPath := filepath.Dir(path)
|
||||
parentInfo, err := os.Stat(parentPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking fs node of parent %q: %w", parentPath, err)
|
||||
} else if !parentInfo.IsDir() {
|
||||
return fmt.Errorf("%q is not a directory", parentPath)
|
||||
}
|
||||
}
|
||||
|
||||
info, err := os.Stat(path)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
// fine
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("checking fs node: %w", err)
|
||||
} else if !info.IsDir() {
|
||||
return fmt.Errorf("exists but is not a directory")
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := os.Mkdir(path, 0700); err != nil {
|
||||
return fmt.Errorf("creating directory: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ import (
|
||||
"isle/admin"
|
||||
"isle/bootstrap"
|
||||
"isle/nebula"
|
||||
"net/netip"
|
||||
"slices"
|
||||
|
||||
"golang.org/x/exp/maps"
|
||||
@ -87,14 +86,21 @@ func (r *RPC) GetHosts(
|
||||
return GetHostsResult{hosts}, nil
|
||||
}
|
||||
|
||||
// GetGarageClientParams passes the call through to the Daemon method of the
|
||||
// same name.
|
||||
// GetGarageClientParams returns a GarageClientParams which can be used to
|
||||
// interact with garage.
|
||||
func (r *RPC) GetGarageClientParams(
|
||||
ctx context.Context, req struct{},
|
||||
) (
|
||||
GarageClientParams, error,
|
||||
bootstrap.GarageClientParams, error,
|
||||
) {
|
||||
return r.daemon.GetGarageClientParams(ctx)
|
||||
b, err := r.daemon.GetBootstrap(ctx)
|
||||
if err != nil {
|
||||
return bootstrap.GarageClientParams{}, fmt.Errorf(
|
||||
"retrieving bootstrap: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
return b.GarageClientParams(), nil
|
||||
}
|
||||
|
||||
// GetNebulaCAPublicCredentials returns the CAPublicCredentials for the network.
|
||||
@ -113,7 +119,7 @@ func (r *RPC) GetNebulaCAPublicCredentials(
|
||||
return b.CAPublicCredentials, nil
|
||||
}
|
||||
|
||||
// RemoveHostRequest contains the arguments to the RemoveHost RPC method.
|
||||
// RemoveHostRequest contains the arguments to the RemoveHost method.
|
||||
//
|
||||
// All fields are required.
|
||||
type RemoveHostRequest struct {
|
||||
@ -124,70 +130,3 @@ type RemoveHostRequest struct {
|
||||
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 {
|
||||
CASigningPrivateKey nebula.SigningPrivateKey // TODO load from secrets storage
|
||||
HostName nebula.HostName
|
||||
IP netip.Addr
|
||||
}
|
||||
|
||||
// CreateHostResult wraps the results from the CreateHost RPC method.
|
||||
type CreateHostResult struct {
|
||||
HostBootstrap bootstrap.Bootstrap
|
||||
}
|
||||
|
||||
// CreateHost passes the call through to the Daemon method of the
|
||||
// same name.
|
||||
func (r *RPC) CreateHost(
|
||||
ctx context.Context, req CreateHostRequest,
|
||||
) (
|
||||
CreateHostResult, error,
|
||||
) {
|
||||
hostBootstrap, err := r.daemon.CreateHost(
|
||||
ctx, req.CASigningPrivateKey, req.HostName, req.IP,
|
||||
)
|
||||
if err != nil {
|
||||
return CreateHostResult{}, err
|
||||
}
|
||||
|
||||
return CreateHostResult{HostBootstrap: hostBootstrap}, nil
|
||||
}
|
||||
|
||||
// CreateNebulaCertificateRequest contains the arguments to the
|
||||
// CreateNebulaCertificate RPC method.
|
||||
//
|
||||
// All fields are required.
|
||||
type CreateNebulaCertificateRequest struct {
|
||||
CASigningPrivateKey nebula.SigningPrivateKey // TODO load from secrets storage
|
||||
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, req.CASigningPrivateKey, req.HostName, req.HostEncryptingPublicKey,
|
||||
)
|
||||
if err != nil {
|
||||
return CreateNebulaCertificateResult{}, err
|
||||
}
|
||||
|
||||
return CreateNebulaCertificateResult{
|
||||
HostNebulaCertifcate: cert,
|
||||
}, nil
|
||||
}
|
||||
|
@ -11,8 +11,6 @@ const (
|
||||
// accessible to all hosts in the network.
|
||||
GlobalBucket = "global-shared"
|
||||
|
||||
// GlobalBucketS3APICredentialsName is the main alias of the shared API key
|
||||
// used to write to the global bucket.
|
||||
GlobalBucketS3APICredentialsName = "global-shared-key"
|
||||
|
||||
// ReplicationFactor indicates the replication factor set on the garage
|
||||
|
@ -1,20 +0,0 @@
|
||||
package garage
|
||||
|
||||
import "isle/secrets"
|
||||
|
||||
var (
|
||||
rpcSecretSecretID = secrets.NewID("garage", "rpc-secret")
|
||||
globalBucketS3APICredentialsSecretID = secrets.NewID("garage", "global-bucket-s3-api-credentials")
|
||||
)
|
||||
|
||||
// Get/Set functions for garage-related secrets.
|
||||
var (
|
||||
GetRPCSecret, SetRPCSecret = secrets.GetSetFunctions[string](
|
||||
rpcSecretSecretID,
|
||||
)
|
||||
|
||||
GetGlobalBucketS3APICredentials,
|
||||
SetGlobalBucketS3APICredentials = secrets.GetSetFunctions[S3APICredentials](
|
||||
globalBucketS3APICredentialsSecretID,
|
||||
)
|
||||
)
|
@ -46,7 +46,7 @@ func (pk *EncryptingPublicKey) UnmarshalText(b []byte) error {
|
||||
// UnmarshalNebulaPEM unmarshals the EncryptingPublicKey as a nebula host public
|
||||
// key PEM.
|
||||
func (pk *EncryptingPublicKey) UnmarshalNebulaPEM(b []byte) error {
|
||||
b, _, err := cert.UnmarshalX25519PublicKey(b)
|
||||
b, _, err := cert.UnmarshalEd25519PublicKey(b)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unmarshaling: %w", err)
|
||||
}
|
||||
|
@ -1,14 +0,0 @@
|
||||
package nebula
|
||||
|
||||
import (
|
||||
"isle/secrets"
|
||||
)
|
||||
|
||||
var (
|
||||
caSigningPrivateKeySecretID = secrets.NewID("nebula", "ca-signing-private-key")
|
||||
)
|
||||
|
||||
// Get/Set functions for the CA SigningPrivateKey secret.
|
||||
var GetCASigningPrivateKey, SetCASigningPrivateKey = secrets.GetSetFunctions[SigningPrivateKey](
|
||||
caSigningPrivateKeySecretID,
|
||||
)
|
@ -1,13 +0,0 @@
|
||||
// Package secrets manages the storage and distributions of secret values that
|
||||
// hosts need to perform various actions.
|
||||
package secrets
|
||||
|
||||
import "fmt"
|
||||
|
||||
// ID is a unique identifier for a Secret.
|
||||
type ID string
|
||||
|
||||
// NewID returns a new ID within the given namespace.
|
||||
func NewID(namespace, id string) ID {
|
||||
return ID(fmt.Sprintf("%s-%s", namespace, id))
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ErrNotFound is returned when an ID could not be found.
|
||||
var ErrNotFound = errors.New("not found")
|
||||
|
||||
// Store is used to persist and retrieve secrets. If a Store serializes a
|
||||
// payload it will do so using JSON.
|
||||
type Store interface {
|
||||
// Set stores the secret payload of the given ID.
|
||||
Set(context.Context, ID, any) error
|
||||
|
||||
// Get retrieves the secret of the given ID, setting it into the given
|
||||
// pointer value, or returns ErrNotFound.
|
||||
Get(context.Context, any, ID) error
|
||||
}
|
||||
|
||||
// GetSetFunctions returns a Get/Set function pair for the given ID and payload
|
||||
// type.
|
||||
func GetSetFunctions[T any](
|
||||
id ID,
|
||||
) (
|
||||
func(context.Context, Store) (T, error), // Get
|
||||
func(context.Context, Store, T) error, // Set
|
||||
) {
|
||||
var (
|
||||
get = func(ctx context.Context, store Store) (T, error) {
|
||||
var v T
|
||||
err := store.Get(ctx, &v, id)
|
||||
return v, err
|
||||
}
|
||||
set = func(ctx context.Context, store Store, v T) error {
|
||||
return store.Set(ctx, id, v)
|
||||
}
|
||||
)
|
||||
return get, set
|
||||
}
|
||||
|
||||
// MultiSet will call Set on the given Store for every key-value pair in the
|
||||
// given map.
|
||||
func MultiSet(ctx context.Context, s Store, m map[ID]any) error {
|
||||
var errs []error
|
||||
for id, payload := range m {
|
||||
if err := s.Set(ctx, id, payload); err != nil {
|
||||
errs = append(errs, fmt.Errorf("setting payload for %q: %w", id, err))
|
||||
}
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// MultiGet will call Get on the given Store for every key-value pair in the
|
||||
// given map. Each value in the map must be a pointer receiver.
|
||||
func MultiGet(ctx context.Context, s Store, m map[ID]any) error {
|
||||
var errs []error
|
||||
for id, into := range m {
|
||||
if err := s.Get(ctx, into, id); err != nil {
|
||||
errs = append(errs, fmt.Errorf("getting payload for %q: %w", id, err))
|
||||
}
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type fsStore struct {
|
||||
dirPath string
|
||||
}
|
||||
|
||||
type fsStorePayload[Body any] struct {
|
||||
Version int
|
||||
Body Body
|
||||
}
|
||||
|
||||
// NewFSStore returns a Store which will store secrets to the given directory.
|
||||
func NewFSStore(dirPath string) (Store, error) {
|
||||
err := os.Mkdir(dirPath, 0700)
|
||||
if err != nil && !errors.Is(err, fs.ErrExist) {
|
||||
return nil, fmt.Errorf("making directory: %w", err)
|
||||
}
|
||||
return &fsStore{dirPath}, nil
|
||||
}
|
||||
|
||||
func (s *fsStore) path(id ID) string {
|
||||
return filepath.Join(s.dirPath, string(id))
|
||||
}
|
||||
|
||||
func (s *fsStore) Set(_ context.Context, id ID, payload any) error {
|
||||
path := s.path(id)
|
||||
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating file %q: %w", path, err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if err := json.NewEncoder(f).Encode(fsStorePayload[any]{
|
||||
Version: 1,
|
||||
Body: payload,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("writing JSON encoded payload to %q: %w", path, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *fsStore) Get(_ context.Context, into any, id ID) error {
|
||||
path := s.path(id)
|
||||
|
||||
f, err := os.Open(path)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return ErrNotFound
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("creating file %q: %w", path, err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var fullPayload fsStorePayload[json.RawMessage]
|
||||
if err := json.NewDecoder(f).Decode(&fullPayload); err != nil {
|
||||
return fmt.Errorf("decoding JSON payload from %q: %w", path, err)
|
||||
}
|
||||
|
||||
if fullPayload.Version != 1 {
|
||||
return fmt.Errorf(
|
||||
"unexpected JSON payload version %d", fullPayload.Version,
|
||||
)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(fullPayload.Body, into); err != nil {
|
||||
return fmt.Errorf(
|
||||
"decoding JSON payload body from %q into %T: %w", path, into, err,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_fsStore(t *testing.T) {
|
||||
type payload struct {
|
||||
Foo int
|
||||
}
|
||||
|
||||
var (
|
||||
ctx = context.Background()
|
||||
dir = t.TempDir()
|
||||
id = NewID("testing", "a")
|
||||
)
|
||||
|
||||
store, err := NewFSStore(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var got payload
|
||||
if err := store.Get(ctx, &got, id); !errors.Is(err, ErrNotFound) {
|
||||
t.Fatalf("expected %v, got: %v", ErrNotFound, err)
|
||||
}
|
||||
|
||||
want := payload{Foo: 5}
|
||||
if err := store.Set(ctx, id, want); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := store.Get(ctx, &got, id); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if want != got {
|
||||
t.Fatalf("wanted %+v, got: %+v", want, got)
|
||||
}
|
||||
}
|
@ -9,6 +9,8 @@ source "$UTILS"/with-1-data-1-empty-node-network.sh
|
||||
[ "$(jq -r <admin.json '.CreationParams.Name')" = "testing" ]
|
||||
[ "$(jq -r <admin.json '.CreationParams.Domain')" = "shared.test" ]
|
||||
|
||||
[ "$(jq -rc <"$BOOTSTRAP_FILE" '.AdminCreationParams')" = "$(jq -rc <admin.json '.CreationParams')" ]
|
||||
[ "$(jq -rc <"$BOOTSTRAP_FILE" '.CAPublicCredentials')" = "$(jq -rc <admin.json '.Nebula.CACredentials.Public')" ]
|
||||
[ "$(jq -r <"$BOOTSTRAP_FILE" '.SignedHostAssigned.Body.Name')" = "primus" ]
|
||||
bootstrap_file="$XDG_STATE_HOME/isle/bootstrap.json"
|
||||
|
||||
[ "$(jq -rc <"$bootstrap_file" '.AdminCreationParams')" = "$(jq -rc <admin.json '.CreationParams')" ]
|
||||
[ "$(jq -rc <"$bootstrap_file" '.CAPublicCredentials')" = "$(jq -rc <admin.json '.Nebula.CACredentials.Public')" ]
|
||||
[ "$(jq -r <"$bootstrap_file" '.SignedHostAssigned.Body.Name')" = "primus" ]
|
||||
|
@ -1,12 +0,0 @@
|
||||
# shellcheck source=../../utils/with-1-data-1-empty-node-network.sh
|
||||
source "$UTILS"/with-1-data-1-empty-node-network.sh
|
||||
|
||||
info="$(isle nebula show)"
|
||||
|
||||
[ "$(echo "$info" | jq -r '.CACert')" \
|
||||
= "$(jq -r <"$BOOTSTRAP_FILE" '.CAPublicCredentials.Cert')" ]
|
||||
|
||||
[ "$(echo "$info" | jq -r '.SubnetCIDR')" = "10.6.9.0/24" ]
|
||||
[ "$(echo "$info" | jq -r '.Lighthouses|length')" = "1" ]
|
||||
[ "$(echo "$info" | jq -r '.Lighthouses[0].PublicAddr')" = "127.0.0.1:60000" ]
|
||||
[ "$(echo "$info" | jq -r '.Lighthouses[0].IP')" = "10.6.9.1" ]
|
@ -1,19 +0,0 @@
|
||||
# shellcheck source=../../utils/with-1-data-1-empty-node-network.sh
|
||||
source "$UTILS"/with-1-data-1-empty-node-network.sh
|
||||
|
||||
nebula-cert keygen -out-key /dev/null -out-pub pubkey
|
||||
cat pubkey
|
||||
|
||||
(
|
||||
isle nebula create-cert \
|
||||
--admin-path admin.json \
|
||||
--hostname non-esiste \
|
||||
--public-key-path pubkey \
|
||||
2>&1 || true \
|
||||
) | grep '\[6\] Host not found'
|
||||
|
||||
isle nebula create-cert \
|
||||
--admin-path admin.json \
|
||||
--hostname primus \
|
||||
--public-key-path pubkey \
|
||||
| grep -- '-----BEGIN NEBULA CERTIFICATE-----'
|
@ -13,6 +13,5 @@ export TMPDIR="$TMPDIR"
|
||||
export XDG_RUNTIME_DIR="$XDG_RUNTIME_DIR"
|
||||
export XDG_STATE_HOME="$XDG_STATE_HOME"
|
||||
export ISLE_DAEMON_HTTP_SOCKET_PATH="$ROOT_TMPDIR/$base-daemon.sock"
|
||||
BOOTSTRAP_FILE="$XDG_STATE_HOME/isle/bootstrap.json"
|
||||
cd "$TMPDIR"
|
||||
EOF
|
||||
|
@ -67,7 +67,7 @@ EOF
|
||||
> admin.json
|
||||
|
||||
echo "Creating secondus bootstrap"
|
||||
isle hosts create \
|
||||
isle admin create-bootstrap \
|
||||
--admin-path admin.json \
|
||||
--hostname secondus \
|
||||
--ip "$secondus_ip" \
|
||||
|
Loading…
Reference in New Issue
Block a user