Propagate garage RPC secret with created host bootstrap
This commit is contained in:
parent
56f796e3fb
commit
86abdb6ae1
@ -6,12 +6,10 @@ import (
|
||||
"crypto/sha512"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"isle/admin"
|
||||
"isle/garage"
|
||||
"isle/nebula"
|
||||
"net/netip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
)
|
||||
@ -36,8 +34,8 @@ type Garage struct {
|
||||
GlobalBucketS3APICredentials garage.S3APICredentials
|
||||
}
|
||||
|
||||
// Bootstrap is used for accessing all information contained within a
|
||||
// bootstrap.json file.
|
||||
// Bootstrap contains all information which is needed by a host daemon to join a
|
||||
// network on boot.
|
||||
type Bootstrap struct {
|
||||
AdminCreationParams admin.CreationParams
|
||||
CAPublicCredentials nebula.CAPublicCredentials
|
||||
@ -89,23 +87,9 @@ func New(
|
||||
}, nil
|
||||
}
|
||||
|
||||
// FromFile reads a bootstrap from a file at the given path. The HostAssigned
|
||||
// field will automatically be unwrapped.
|
||||
func FromFile(path string) (Bootstrap, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return Bootstrap{}, fmt.Errorf("opening file: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var b Bootstrap
|
||||
if err := json.NewDecoder(f).Decode(&b); err != nil {
|
||||
return Bootstrap{}, fmt.Errorf("decoding json: %w", err)
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface. It will
|
||||
// automatically populate the HostAssigned field by unwrapping the
|
||||
// SignedHostAssigned field.
|
||||
func (b *Bootstrap) UnmarshalJSON(data []byte) error {
|
||||
type inner Bootstrap
|
||||
|
||||
@ -124,11 +108,6 @@ func (b *Bootstrap) UnmarshalJSON(data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteTo writes the Bootstrap as a new bootstrap to the given io.Writer.
|
||||
func (b Bootstrap) WriteTo(into io.Writer) error {
|
||||
return json.NewEncoder(into).Encode(b)
|
||||
}
|
||||
|
||||
// ThisHost is a shortcut for b.Hosts[b.HostName], but will panic if the
|
||||
// HostName isn't found in the Hosts map.
|
||||
func (b Bootstrap) ThisHost() Host {
|
||||
|
@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"isle/bootstrap"
|
||||
@ -67,7 +68,7 @@ var subCmdHostsCreate = subCmd{
|
||||
return fmt.Errorf("calling CreateHost: %w", err)
|
||||
}
|
||||
|
||||
return res.HostBootstrap.WriteTo(os.Stdout)
|
||||
return json.NewEncoder(os.Stdout).Encode(res.JoiningBootstrap)
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -4,8 +4,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"isle/admin"
|
||||
"isle/bootstrap"
|
||||
"isle/daemon"
|
||||
"isle/jsonutil"
|
||||
"os"
|
||||
)
|
||||
|
||||
@ -88,8 +88,8 @@ var subCmdNetworkJoin = subCmd{
|
||||
return errors.New("--bootstrap-path is required")
|
||||
}
|
||||
|
||||
newBootstrap, err := bootstrap.FromFile(*bootstrapPath)
|
||||
if err != nil {
|
||||
var newBootstrap daemon.JoiningBootstrap
|
||||
if err := jsonutil.LoadFile(&newBootstrap, *bootstrapPath); err != nil {
|
||||
return fmt.Errorf(
|
||||
"loading bootstrap from %q: %w", *bootstrapPath, err,
|
||||
)
|
||||
|
@ -1,14 +1,24 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"isle/bootstrap"
|
||||
"isle/garage/garagesrv"
|
||||
"isle/jsonutil"
|
||||
"isle/secrets"
|
||||
)
|
||||
|
||||
// JoiningBootstrap wraps a normal Bootstrap to include extra data which a host
|
||||
// might need while joining a network.
|
||||
type JoiningBootstrap struct {
|
||||
Bootstrap bootstrap.Bootstrap
|
||||
Secrets map[secrets.ID]json.RawMessage
|
||||
}
|
||||
|
||||
func writeBootstrapToStateDir(
|
||||
stateDirPath string, hostBootstrap bootstrap.Bootstrap,
|
||||
) error {
|
||||
@ -21,14 +31,11 @@ func writeBootstrapToStateDir(
|
||||
return fmt.Errorf("creating directory %q: %w", dirPath, err)
|
||||
}
|
||||
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating file %q: %w", path, err)
|
||||
if err := jsonutil.WriteFile(hostBootstrap, path, 0700); err != nil {
|
||||
return fmt.Errorf("writing bootstrap to %q: %w", path, err)
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
return hostBootstrap.WriteTo(f)
|
||||
return nil
|
||||
}
|
||||
|
||||
func coalesceDaemonConfigAndBootstrap(
|
||||
|
@ -5,7 +5,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"isle/bootstrap"
|
||||
"isle/garage"
|
||||
"isle/secrets"
|
||||
|
||||
"code.betamike.com/micropelago/pmux/pmuxlib"
|
||||
@ -41,7 +40,7 @@ func NewChildren(
|
||||
opts = opts.withDefaults()
|
||||
|
||||
logger.Info(ctx, "Loading secrets")
|
||||
garageRPCSecret, err := garage.GetRPCSecret(ctx, secretsStore)
|
||||
garageRPCSecret, err := getGarageRPCSecret(ctx, secretsStore)
|
||||
if err != nil && !errors.Is(err, secrets.ErrNotFound) {
|
||||
return nil, fmt.Errorf("loading garage RPC secret: %w", err)
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
"isle/admin"
|
||||
"isle/bootstrap"
|
||||
"isle/garage"
|
||||
"isle/jsonutil"
|
||||
"isle/nebula"
|
||||
"isle/secrets"
|
||||
"net/netip"
|
||||
@ -50,7 +51,7 @@ type Daemon interface {
|
||||
//
|
||||
// Errors:
|
||||
// - ErrAlreadyJoined
|
||||
JoinNetwork(context.Context, bootstrap.Bootstrap) error
|
||||
JoinNetwork(context.Context, JoiningBootstrap) error
|
||||
|
||||
// GetBootstraps returns the currently active Bootstrap.
|
||||
GetBootstrap(context.Context) (bootstrap.Bootstrap, error)
|
||||
@ -70,7 +71,7 @@ type Daemon interface {
|
||||
hostName nebula.HostName,
|
||||
ip netip.Addr, // TODO automatically choose IP address
|
||||
) (
|
||||
bootstrap.Bootstrap, error,
|
||||
JoiningBootstrap, error,
|
||||
)
|
||||
|
||||
// CreateNebulaCertificate creates and signs a new nebula certficate for an
|
||||
@ -200,7 +201,8 @@ func NewDaemon(
|
||||
)
|
||||
}
|
||||
|
||||
currBootstrap, err := bootstrap.FromFile(bootstrapFilePath)
|
||||
var currBootstrap bootstrap.Bootstrap
|
||||
err = jsonutil.LoadFile(&currBootstrap, bootstrapFilePath)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
// daemon has never had a network created or joined
|
||||
} else if err != nil {
|
||||
@ -538,7 +540,7 @@ func (d *daemon) CreateNetwork(
|
||||
|
||||
garageRPCSecret := randStr(32)
|
||||
|
||||
err = garage.SetRPCSecret(ctx, d.secretsStore, garageRPCSecret)
|
||||
err = setGarageRPCSecret(ctx, d.secretsStore, garageRPCSecret)
|
||||
if err != nil {
|
||||
return admin.Admin{}, fmt.Errorf("storing garage RPC secret: %w", err)
|
||||
}
|
||||
@ -597,7 +599,7 @@ func (d *daemon) CreateNetwork(
|
||||
}
|
||||
|
||||
func (d *daemon) JoinNetwork(
|
||||
ctx context.Context, newBootstrap bootstrap.Bootstrap,
|
||||
ctx context.Context, newBootstrap JoiningBootstrap,
|
||||
) error {
|
||||
d.l.Lock()
|
||||
|
||||
@ -608,7 +610,13 @@ func (d *daemon) JoinNetwork(
|
||||
|
||||
readyCh := make(chan struct{}, 1)
|
||||
|
||||
err := d.initialize(newBootstrap, readyCh)
|
||||
err := secrets.Import(ctx, d.secretsStore, newBootstrap.Secrets)
|
||||
if err != nil {
|
||||
d.l.Unlock()
|
||||
return fmt.Errorf("importing secrets: %w", err)
|
||||
}
|
||||
|
||||
err = d.initialize(newBootstrap.Bootstrap, readyCh)
|
||||
d.l.Unlock()
|
||||
if err != nil {
|
||||
return fmt.Errorf("initializing daemon: %w", err)
|
||||
@ -681,21 +689,26 @@ func (d *daemon) CreateHost(
|
||||
hostName nebula.HostName,
|
||||
ip netip.Addr,
|
||||
) (
|
||||
bootstrap.Bootstrap, error,
|
||||
JoiningBootstrap, error,
|
||||
) {
|
||||
return withCurrBootstrap(d, func(
|
||||
currBootstrap bootstrap.Bootstrap,
|
||||
) (
|
||||
bootstrap.Bootstrap, error,
|
||||
JoiningBootstrap, error,
|
||||
) {
|
||||
garageGlobalBucketS3APICreds := currBootstrap.Garage.GlobalBucketS3APICredentials
|
||||
var (
|
||||
garageGlobalBucketS3APICreds = currBootstrap.Garage.GlobalBucketS3APICredentials
|
||||
|
||||
garageBootstrap := bootstrap.Garage{
|
||||
AdminToken: randStr(32),
|
||||
GlobalBucketS3APICredentials: garageGlobalBucketS3APICreds,
|
||||
}
|
||||
garageBootstrap = bootstrap.Garage{
|
||||
AdminToken: randStr(32),
|
||||
GlobalBucketS3APICredentials: garageGlobalBucketS3APICreds,
|
||||
}
|
||||
|
||||
newHostBootstrap, err := bootstrap.New(
|
||||
joiningBootstrap JoiningBootstrap
|
||||
err error
|
||||
)
|
||||
|
||||
joiningBootstrap.Bootstrap, err = bootstrap.New(
|
||||
makeCACreds(currBootstrap, caSigningPrivateKey),
|
||||
currBootstrap.AdminCreationParams,
|
||||
garageBootstrap,
|
||||
@ -703,17 +716,25 @@ func (d *daemon) CreateHost(
|
||||
ip,
|
||||
)
|
||||
if err != nil {
|
||||
return bootstrap.Bootstrap{}, fmt.Errorf(
|
||||
return JoiningBootstrap{}, fmt.Errorf(
|
||||
"initializing bootstrap data: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
newHostBootstrap.Hosts = currBootstrap.Hosts
|
||||
joiningBootstrap.Bootstrap.Hosts = currBootstrap.Hosts
|
||||
|
||||
if joiningBootstrap.Secrets, err = secrets.Export(
|
||||
ctx, d.secretsStore, []secrets.ID{
|
||||
garageRPCSecretSecretID,
|
||||
},
|
||||
); err != nil {
|
||||
return JoiningBootstrap{}, fmt.Errorf("exporting secrets: %w", err)
|
||||
}
|
||||
|
||||
// TODO persist new bootstrap to garage. Requires making the daemon
|
||||
// config change watching logic smarter, so only dnsmasq gets restarted.
|
||||
|
||||
return newHostBootstrap, nil
|
||||
return joiningBootstrap, nil
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -24,7 +24,7 @@ func (d *daemon) getGarageClientParams(
|
||||
) (
|
||||
GarageClientParams, error,
|
||||
) {
|
||||
rpcSecret, err := garage.GetRPCSecret(ctx, d.secretsStore)
|
||||
rpcSecret, err := getGarageRPCSecret(ctx, d.secretsStore)
|
||||
if err != nil && !errors.Is(err, secrets.ErrNotFound) {
|
||||
return GarageClientParams{}, fmt.Errorf("getting garage rpc secret: %w", err)
|
||||
}
|
||||
|
@ -56,7 +56,7 @@ func (r *RPC) CreateNetwork(
|
||||
|
||||
// JoinNetwork passes through to the Daemon method of the same name.
|
||||
func (r *RPC) JoinNetwork(
|
||||
ctx context.Context, req bootstrap.Bootstrap,
|
||||
ctx context.Context, req JoiningBootstrap,
|
||||
) (
|
||||
struct{}, error,
|
||||
) {
|
||||
@ -137,7 +137,7 @@ type CreateHostRequest struct {
|
||||
|
||||
// CreateHostResult wraps the results from the CreateHost RPC method.
|
||||
type CreateHostResult struct {
|
||||
HostBootstrap bootstrap.Bootstrap
|
||||
JoiningBootstrap JoiningBootstrap
|
||||
}
|
||||
|
||||
// CreateHost passes the call through to the Daemon method of the
|
||||
@ -147,14 +147,14 @@ func (r *RPC) CreateHost(
|
||||
) (
|
||||
CreateHostResult, error,
|
||||
) {
|
||||
hostBootstrap, err := r.daemon.CreateHost(
|
||||
joiningBootstrap, err := r.daemon.CreateHost(
|
||||
ctx, req.CASigningPrivateKey, req.HostName, req.IP,
|
||||
)
|
||||
if err != nil {
|
||||
return CreateHostResult{}, err
|
||||
}
|
||||
|
||||
return CreateHostResult{HostBootstrap: hostBootstrap}, nil
|
||||
return CreateHostResult{JoiningBootstrap: joiningBootstrap}, nil
|
||||
}
|
||||
|
||||
// CreateNebulaCertificateRequest contains the arguments to the
|
||||
|
39
go/daemon/secrets.go
Normal file
39
go/daemon/secrets.go
Normal file
@ -0,0 +1,39 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"isle/garage"
|
||||
"isle/secrets"
|
||||
)
|
||||
|
||||
const (
|
||||
secretsNSGarage = "garage"
|
||||
)
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Garage-related secrets
|
||||
|
||||
func garageS3APIBucketCredentialsSecretID(credsName string) secrets.ID {
|
||||
return secrets.NewID(
|
||||
secretsNSGarage, fmt.Sprintf("s3-api-bucket-credentials-%s", credsName),
|
||||
)
|
||||
}
|
||||
|
||||
var (
|
||||
garageRPCSecretSecretID = secrets.NewID(secretsNSGarage, "rpc-secret")
|
||||
garageS3APIGlobalBucketCredentialsSecretID = garageS3APIBucketCredentialsSecretID(
|
||||
garage.GlobalBucketS3APICredentialsName,
|
||||
)
|
||||
)
|
||||
|
||||
// Get/Set functions for garage-related secrets.
|
||||
var (
|
||||
getGarageRPCSecret, setGarageRPCSecret = secrets.GetSetFunctions[string](
|
||||
garageRPCSecretSecretID,
|
||||
)
|
||||
|
||||
getGarageS3APIGlobalBucketCredentials,
|
||||
setGarageS3APIGlobalBucketCredentials = secrets.GetSetFunctions[garage.S3APICredentials](
|
||||
garageS3APIGlobalBucketCredentialsSecretID,
|
||||
)
|
||||
)
|
@ -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,
|
||||
)
|
||||
)
|
@ -2,6 +2,7 @@ package secrets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
@ -64,3 +65,41 @@ func MultiGet(ctx context.Context, s Store, m map[ID]any) error {
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// Export returns a map of ID to raw payload for each ID given. An error is
|
||||
// returned for _each_ ID which could not be exported, wrapped using
|
||||
// `errors.Join`, alongside whatever keys could be exported.
|
||||
func Export(
|
||||
ctx context.Context, s Store, ids []ID,
|
||||
) (
|
||||
map[ID]json.RawMessage, error,
|
||||
) {
|
||||
var (
|
||||
m = map[ID]json.RawMessage{}
|
||||
errs []error
|
||||
)
|
||||
|
||||
for _, id := range ids {
|
||||
var into json.RawMessage
|
||||
if err := s.Get(ctx, &into, id); err != nil {
|
||||
errs = append(errs, fmt.Errorf("exporting %q: %w", id, err))
|
||||
continue
|
||||
}
|
||||
m[id] = into
|
||||
}
|
||||
|
||||
return m, errors.Join(errs...)
|
||||
}
|
||||
|
||||
// Import sets all given ID/payload pairs into the Store.
|
||||
func Import(
|
||||
ctx context.Context, s Store, m map[ID]json.RawMessage,
|
||||
) error {
|
||||
var errs []error
|
||||
for id, payload := range m {
|
||||
if err := s.Set(ctx, id, payload); err != nil {
|
||||
errs = append(errs, fmt.Errorf("importing %q: %w", id, err))
|
||||
}
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
@ -1,14 +0,0 @@
|
||||
# shellcheck source=../../utils/with-1-data-1-empty-node-network.sh
|
||||
source "$UTILS"/with-1-data-1-empty-node-network.sh
|
||||
|
||||
adminBS="$XDG_STATE_HOME"/isle/bootstrap.json
|
||||
bs="$secondus_bootstrap" # set in with-1-data-1-empty-node-network.sh
|
||||
|
||||
[ "$(jq -r <"$bs" '.AdminCreationParams')" = "$(jq -r <admin.json '.CreationParams')" ]
|
||||
[ "$(jq -r <"$bs" '.SignedHostAssigned.Body.Name')" = "secondus" ]
|
||||
|
||||
[ "$(jq -r <"$bs" '.Hosts.primus.PublicCredentials')" \
|
||||
= "$(jq -r <"$adminBS" '.SignedHostAssigned.Body.PublicCredentials')" ]
|
||||
|
||||
[ "$(jq <"$bs" '.Hosts.primus.Garage.Instances|length')" = "3" ]
|
||||
|
16
tests/cases/hosts/01-create.sh
Normal file
16
tests/cases/hosts/01-create.sh
Normal file
@ -0,0 +1,16 @@
|
||||
# shellcheck source=../../utils/with-1-data-1-empty-node-network.sh
|
||||
source "$UTILS"/with-1-data-1-empty-node-network.sh
|
||||
|
||||
adminBS="$XDG_STATE_HOME"/isle/bootstrap.json
|
||||
bs="$secondus_bootstrap" # set in with-1-data-1-empty-node-network.sh
|
||||
|
||||
[ "$(jq -r <"$bs" '.Bootstrap.AdminCreationParams')" = "$(jq -r <admin.json '.CreationParams')" ]
|
||||
[ "$(jq -r <"$bs" '.Bootstrap.SignedHostAssigned.Body.Name')" = "secondus" ]
|
||||
|
||||
[ "$(jq -r <"$bs" '.Bootstrap.Hosts.primus.PublicCredentials')" \
|
||||
= "$(jq -r <"$adminBS" '.SignedHostAssigned.Body.PublicCredentials')" ]
|
||||
|
||||
[ "$(jq <"$bs" '.Bootstrap.Hosts.primus.Garage.Instances|length')" = "3" ]
|
||||
|
||||
[ "$(jq <"$bs" '.Secrets["garage-rpc-secret"]')" != "null" ]
|
||||
|
Loading…
Reference in New Issue
Block a user