Implement basic secrets architecture, use it for garage RPC secret
This commit is contained in:
parent
b5059be7fa
commit
56f796e3fb
@ -25,7 +25,6 @@ type Admin struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Garage struct {
|
Garage struct {
|
||||||
RPCSecret string
|
|
||||||
GlobalBucketS3APICredentials garage.S3APICredentials
|
GlobalBucketS3APICredentials garage.S3APICredentials
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,10 +30,6 @@ func AppDirPath(appDirPath string) string {
|
|||||||
|
|
||||||
// Garage contains parameters needed to connect to and use the garage cluster.
|
// Garage contains parameters needed to connect to and use the garage cluster.
|
||||||
type Garage struct {
|
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
|
AdminToken string
|
||||||
|
|
||||||
// TODO this should be part of admin.CreationParams
|
// TODO this should be part of admin.CreationParams
|
||||||
|
@ -4,24 +4,6 @@ import (
|
|||||||
"isle/garage"
|
"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.
|
// GaragePeers returns a Peer for each known garage instance in the network.
|
||||||
func (b Bootstrap) GaragePeers() []garage.RemotePeer {
|
func (b Bootstrap) GaragePeers() []garage.RemotePeer {
|
||||||
|
|
||||||
@ -69,12 +51,3 @@ func (b Bootstrap) ChooseGaragePeer() garage.RemotePeer {
|
|||||||
|
|
||||||
panic("no garage instances configured")
|
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"isle/bootstrap"
|
"isle/daemon"
|
||||||
)
|
)
|
||||||
|
|
||||||
// minio-client keeps a configuration directory which contains various pieces of
|
// minio-client keeps a configuration directory which contains various pieces of
|
||||||
@ -52,7 +53,7 @@ var subCmdGarageMC = subCmd{
|
|||||||
return fmt.Errorf("parsing flags: %w", err)
|
return fmt.Errorf("parsing flags: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var clientParams bootstrap.GarageClientParams
|
var clientParams daemon.GarageClientParams
|
||||||
err := subCmdCtx.daemonRCPClient.Call(
|
err := subCmdCtx.daemonRCPClient.Call(
|
||||||
subCmdCtx.ctx, &clientParams, "GetGarageClientParams", nil,
|
subCmdCtx.ctx, &clientParams, "GetGarageClientParams", nil,
|
||||||
)
|
)
|
||||||
@ -119,7 +120,7 @@ var subCmdGarageCLI = subCmd{
|
|||||||
descr: "Runs the garage binary, automatically configured to point to the garage sub-process of a running isle daemon",
|
descr: "Runs the garage binary, automatically configured to point to the garage sub-process of a running isle daemon",
|
||||||
do: func(subCmdCtx subCmdCtx) error {
|
do: func(subCmdCtx subCmdCtx) error {
|
||||||
|
|
||||||
var clientParams bootstrap.GarageClientParams
|
var clientParams daemon.GarageClientParams
|
||||||
err := subCmdCtx.daemonRCPClient.Call(
|
err := subCmdCtx.daemonRCPClient.Call(
|
||||||
subCmdCtx.ctx, &clientParams, "GetGarageClientParams", nil,
|
subCmdCtx.ctx, &clientParams, "GetGarageClientParams", nil,
|
||||||
)
|
)
|
||||||
@ -127,6 +128,10 @@ var subCmdGarageCLI = subCmd{
|
|||||||
return fmt.Errorf("calling GetGarageClientParams: %w", err)
|
return fmt.Errorf("calling GetGarageClientParams: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if clientParams.RPCSecret == "" {
|
||||||
|
return errors.New("this host does not have the garage RPC secret")
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
binPath = binPath("garage")
|
binPath = binPath("garage")
|
||||||
args = append([]string{"garage"}, subCmdCtx.args...)
|
args = append([]string{"garage"}, subCmdCtx.args...)
|
||||||
|
@ -101,7 +101,7 @@ func bootstrapGarageHostForAlloc(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func garageWriteChildConfig(
|
func garageWriteChildConfig(
|
||||||
runtimeDirPath string,
|
rpcSecret, runtimeDirPath string,
|
||||||
hostBootstrap bootstrap.Bootstrap,
|
hostBootstrap bootstrap.Bootstrap,
|
||||||
alloc ConfigStorageAllocation,
|
alloc ConfigStorageAllocation,
|
||||||
) (
|
) (
|
||||||
@ -129,7 +129,7 @@ func garageWriteChildConfig(
|
|||||||
MetaPath: alloc.MetaPath,
|
MetaPath: alloc.MetaPath,
|
||||||
DataPath: alloc.DataPath,
|
DataPath: alloc.DataPath,
|
||||||
|
|
||||||
RPCSecret: hostBootstrap.Garage.RPCSecret,
|
RPCSecret: rpcSecret,
|
||||||
AdminToken: hostBootstrap.Garage.AdminToken,
|
AdminToken: hostBootstrap.Garage.AdminToken,
|
||||||
|
|
||||||
LocalPeer: peer,
|
LocalPeer: peer,
|
||||||
@ -144,20 +144,28 @@ func garageWriteChildConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func garagePmuxProcConfigs(
|
func garagePmuxProcConfigs(
|
||||||
|
ctx context.Context,
|
||||||
logger *mlog.Logger,
|
logger *mlog.Logger,
|
||||||
runtimeDirPath, binDirPath string,
|
rpcSecret, runtimeDirPath, binDirPath string,
|
||||||
daemonConfig Config,
|
daemonConfig Config,
|
||||||
hostBootstrap bootstrap.Bootstrap,
|
hostBootstrap bootstrap.Bootstrap,
|
||||||
) (
|
) (
|
||||||
[]pmuxlib.ProcessConfig, error,
|
[]pmuxlib.ProcessConfig, error,
|
||||||
) {
|
) {
|
||||||
|
var (
|
||||||
|
pmuxProcConfigs []pmuxlib.ProcessConfig
|
||||||
|
allocs = daemonConfig.Storage.Allocations
|
||||||
|
)
|
||||||
|
|
||||||
var pmuxProcConfigs []pmuxlib.ProcessConfig
|
if len(allocs) > 0 && rpcSecret == "" {
|
||||||
|
logger.WarnString(ctx, "Not starting garage instances for storage allocations, missing garage RPC secret")
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
for _, alloc := range daemonConfig.Storage.Allocations {
|
for _, alloc := range allocs {
|
||||||
|
|
||||||
childConfigPath, err := garageWriteChildConfig(
|
childConfigPath, err := garageWriteChildConfig(
|
||||||
runtimeDirPath, hostBootstrap, alloc,
|
rpcSecret, runtimeDirPath, hostBootstrap, alloc,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("writing child config file for alloc %+v: %w", alloc, err)
|
return nil, fmt.Errorf("writing child config file for alloc %+v: %w", alloc, err)
|
||||||
@ -176,9 +184,7 @@ func garagePmuxProcConfigs(
|
|||||||
return pmuxProcConfigs, nil
|
return pmuxProcConfigs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO don't expose this publicly once cluster creation is done via Daemon
|
func garageApplyLayout(
|
||||||
// interface.
|
|
||||||
func GarageApplyLayout(
|
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
logger *mlog.Logger,
|
logger *mlog.Logger,
|
||||||
daemonConfig Config,
|
daemonConfig Config,
|
||||||
|
@ -9,8 +9,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (c *Children) newPmuxConfig(
|
func (c *Children) newPmuxConfig(
|
||||||
binDirPath string, daemonConfig Config, hostBootstrap bootstrap.Bootstrap,
|
ctx context.Context,
|
||||||
) (pmuxlib.Config, error) {
|
garageRPCSecret, binDirPath string,
|
||||||
|
daemonConfig Config,
|
||||||
|
hostBootstrap bootstrap.Bootstrap,
|
||||||
|
) (
|
||||||
|
pmuxlib.Config, error,
|
||||||
|
) {
|
||||||
nebulaPmuxProcConfig, err := nebulaPmuxProcConfig(
|
nebulaPmuxProcConfig, err := nebulaPmuxProcConfig(
|
||||||
c.opts.EnvVars.RuntimeDirPath,
|
c.opts.EnvVars.RuntimeDirPath,
|
||||||
binDirPath,
|
binDirPath,
|
||||||
@ -34,7 +39,9 @@ func (c *Children) newPmuxConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
garagePmuxProcConfigs, err := garagePmuxProcConfigs(
|
garagePmuxProcConfigs, err := garagePmuxProcConfigs(
|
||||||
|
ctx,
|
||||||
c.logger,
|
c.logger,
|
||||||
|
garageRPCSecret,
|
||||||
c.opts.EnvVars.RuntimeDirPath,
|
c.opts.EnvVars.RuntimeDirPath,
|
||||||
binDirPath,
|
binDirPath,
|
||||||
daemonConfig,
|
daemonConfig,
|
||||||
|
@ -2,8 +2,11 @@ package daemon
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"isle/bootstrap"
|
"isle/bootstrap"
|
||||||
|
"isle/garage"
|
||||||
|
"isle/secrets"
|
||||||
|
|
||||||
"code.betamike.com/micropelago/pmux/pmuxlib"
|
"code.betamike.com/micropelago/pmux/pmuxlib"
|
||||||
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
|
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
|
||||||
@ -27,15 +30,22 @@ type Children struct {
|
|||||||
func NewChildren(
|
func NewChildren(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
logger *mlog.Logger,
|
logger *mlog.Logger,
|
||||||
|
binDirPath string,
|
||||||
|
secretsStore secrets.Store,
|
||||||
daemonConfig Config,
|
daemonConfig Config,
|
||||||
hostBootstrap bootstrap.Bootstrap,
|
hostBootstrap bootstrap.Bootstrap,
|
||||||
binDirPath string,
|
|
||||||
opts *Opts,
|
opts *Opts,
|
||||||
) (
|
) (
|
||||||
*Children, error,
|
*Children, error,
|
||||||
) {
|
) {
|
||||||
opts = opts.withDefaults()
|
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())
|
pmuxCtx, pmuxCancelFn := context.WithCancel(context.Background())
|
||||||
|
|
||||||
c := &Children{
|
c := &Children{
|
||||||
@ -45,7 +55,9 @@ func NewChildren(
|
|||||||
pmuxStoppedCh: make(chan struct{}),
|
pmuxStoppedCh: make(chan struct{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
pmuxConfig, err := c.newPmuxConfig(binDirPath, daemonConfig, hostBootstrap)
|
pmuxConfig, err := c.newPmuxConfig(
|
||||||
|
ctx, garageRPCSecret, binDirPath, daemonConfig, hostBootstrap,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("generating pmux config: %w", err)
|
return nil, fmt.Errorf("generating pmux config: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -13,8 +13,10 @@ import (
|
|||||||
"isle/bootstrap"
|
"isle/bootstrap"
|
||||||
"isle/garage"
|
"isle/garage"
|
||||||
"isle/nebula"
|
"isle/nebula"
|
||||||
|
"isle/secrets"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -53,6 +55,10 @@ type Daemon interface {
|
|||||||
// GetBootstraps returns the currently active Bootstrap.
|
// GetBootstraps returns the currently active Bootstrap.
|
||||||
GetBootstrap(context.Context) (bootstrap.Bootstrap, error)
|
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 removes the host of the given name from the network.
|
||||||
RemoveHost(context.Context, nebula.HostName) error
|
RemoveHost(context.Context, nebula.HostName) error
|
||||||
|
|
||||||
@ -134,6 +140,8 @@ type daemon struct {
|
|||||||
envBinDirPath string
|
envBinDirPath string
|
||||||
opts *Opts
|
opts *Opts
|
||||||
|
|
||||||
|
secretsStore secrets.Store
|
||||||
|
|
||||||
l sync.RWMutex
|
l sync.RWMutex
|
||||||
state int
|
state int
|
||||||
children *Children
|
children *Children
|
||||||
@ -181,6 +189,17 @@ func NewDaemon(
|
|||||||
return nil, fmt.Errorf("initializing daemon directories: %w", err)
|
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)
|
currBootstrap, err := bootstrap.FromFile(bootstrapFilePath)
|
||||||
if errors.Is(err, fs.ErrNotExist) {
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
// daemon has never had a network created or joined
|
// daemon has never had a network created or joined
|
||||||
@ -276,7 +295,7 @@ func (d *daemon) checkBootstrap(
|
|||||||
|
|
||||||
thisHost := hostBootstrap.ThisHost()
|
thisHost := hostBootstrap.ThisHost()
|
||||||
|
|
||||||
newHosts, err := getGarageBootstrapHosts(ctx, d.logger, hostBootstrap)
|
newHosts, err := d.getGarageBootstrapHosts(ctx, d.logger, hostBootstrap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return bootstrap.Bootstrap{}, false, fmt.Errorf("getting hosts from garage: %w", err)
|
return bootstrap.Bootstrap{}, false, fmt.Errorf("getting hosts from garage: %w", err)
|
||||||
}
|
}
|
||||||
@ -348,7 +367,7 @@ func (d *daemon) postInit(ctx context.Context) bool {
|
|||||||
d.logger,
|
d.logger,
|
||||||
"Applying garage layout",
|
"Applying garage layout",
|
||||||
func(ctx context.Context) error {
|
func(ctx context.Context) error {
|
||||||
return GarageApplyLayout(
|
return garageApplyLayout(
|
||||||
ctx, d.logger, d.daemonConfig, d.currBootstrap,
|
ctx, d.logger, d.daemonConfig, d.currBootstrap,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@ -402,7 +421,7 @@ func (d *daemon) postInit(ctx context.Context) bool {
|
|||||||
d.logger,
|
d.logger,
|
||||||
"Updating host info in garage",
|
"Updating host info in garage",
|
||||||
func(ctx context.Context) error {
|
func(ctx context.Context) error {
|
||||||
return putGarageBoostrapHost(ctx, d.logger, d.currBootstrap)
|
return d.putGarageBoostrapHost(ctx, d.logger, d.currBootstrap)
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
return false
|
return false
|
||||||
@ -437,15 +456,16 @@ func (d *daemon) restartLoop(ctx context.Context, readyCh chan<- struct{}) {
|
|||||||
children, err := NewChildren(
|
children, err := NewChildren(
|
||||||
ctx,
|
ctx,
|
||||||
d.logger.WithNamespace("children"),
|
d.logger.WithNamespace("children"),
|
||||||
|
d.envBinDirPath,
|
||||||
|
d.secretsStore,
|
||||||
d.daemonConfig,
|
d.daemonConfig,
|
||||||
d.currBootstrap,
|
d.currBootstrap,
|
||||||
d.envBinDirPath,
|
|
||||||
d.opts,
|
d.opts,
|
||||||
)
|
)
|
||||||
if errors.Is(err, context.Canceled) {
|
if errors.Is(err, context.Canceled) {
|
||||||
return
|
return
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
d.logger.Error(ctx, "failed to initialize daemon", err)
|
d.logger.Error(ctx, "failed to initialize child processes", err)
|
||||||
if !wait(1 * time.Second) {
|
if !wait(1 * time.Second) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -502,17 +522,25 @@ func (d *daemon) CreateNetwork(
|
|||||||
return admin.Admin{}, fmt.Errorf("creating nebula CA cert: %w", err)
|
return admin.Admin{}, fmt.Errorf("creating nebula CA cert: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
adm := admin.Admin{
|
var (
|
||||||
CreationParams: admin.CreationParams{
|
adm = admin.Admin{
|
||||||
ID: randStr(32),
|
CreationParams: admin.CreationParams{
|
||||||
Name: name,
|
ID: randStr(32),
|
||||||
Domain: domain,
|
Name: name,
|
||||||
},
|
Domain: domain,
|
||||||
}
|
},
|
||||||
|
}
|
||||||
|
|
||||||
garageBootstrap := bootstrap.Garage{
|
garageBootstrap = bootstrap.Garage{
|
||||||
RPCSecret: randStr(32),
|
AdminToken: randStr(32),
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
hostBootstrap, err := bootstrap.New(
|
hostBootstrap, err := bootstrap.New(
|
||||||
@ -563,7 +591,6 @@ func (d *daemon) CreateNetwork(
|
|||||||
d.l.RUnlock()
|
d.l.RUnlock()
|
||||||
|
|
||||||
adm.Nebula.CACredentials = nebulaCACreds
|
adm.Nebula.CACredentials = nebulaCACreds
|
||||||
adm.Garage.RPCSecret = hostBootstrap.Garage.RPCSecret
|
|
||||||
adm.Garage.GlobalBucketS3APICredentials = garageGlobalBucketCreds
|
adm.Garage.GlobalBucketS3APICredentials = garageGlobalBucketCreds
|
||||||
|
|
||||||
return adm, nil
|
return adm, nil
|
||||||
@ -605,6 +632,20 @@ 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 {
|
func (d *daemon) RemoveHost(ctx context.Context, hostName nebula.HostName) error {
|
||||||
// TODO RemoveHost should publish a certificate revocation for the host
|
// TODO RemoveHost should publish a certificate revocation for the host
|
||||||
// being removed.
|
// being removed.
|
||||||
@ -613,7 +654,12 @@ func (d *daemon) RemoveHost(ctx context.Context, hostName nebula.HostName) error
|
|||||||
) (
|
) (
|
||||||
struct{}, error,
|
struct{}, error,
|
||||||
) {
|
) {
|
||||||
client := currBootstrap.GarageClientParams().GlobalBucketS3APIClient()
|
garageClientParams, err := d.getGarageClientParams(ctx, currBootstrap)
|
||||||
|
if err != nil {
|
||||||
|
return struct{}{}, fmt.Errorf("get garage client params: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := garageClientParams.GlobalBucketS3APIClient()
|
||||||
return struct{}{}, removeGarageBootstrapHost(ctx, client, hostName)
|
return struct{}{}, removeGarageBootstrapHost(ctx, client, hostName)
|
||||||
})
|
})
|
||||||
return err
|
return err
|
||||||
@ -642,15 +688,9 @@ func (d *daemon) CreateHost(
|
|||||||
) (
|
) (
|
||||||
bootstrap.Bootstrap, error,
|
bootstrap.Bootstrap, error,
|
||||||
) {
|
) {
|
||||||
var (
|
garageGlobalBucketS3APICreds := currBootstrap.Garage.GlobalBucketS3APICredentials
|
||||||
garageRPCSecret = currBootstrap.Garage.RPCSecret
|
|
||||||
garageGlobalBucketS3APICreds = currBootstrap.Garage.GlobalBucketS3APICredentials
|
|
||||||
)
|
|
||||||
|
|
||||||
// TODO check if garageRPCSecret is actually set
|
|
||||||
|
|
||||||
garageBootstrap := bootstrap.Garage{
|
garageBootstrap := bootstrap.Garage{
|
||||||
RPCSecret: garageRPCSecret,
|
|
||||||
AdminToken: randStr(32),
|
AdminToken: randStr(32),
|
||||||
GlobalBucketS3APICredentials: garageGlobalBucketS3APICreds,
|
GlobalBucketS3APICredentials: garageGlobalBucketS3APICreds,
|
||||||
}
|
}
|
||||||
|
@ -22,34 +22,6 @@ type EnvVars struct {
|
|||||||
|
|
||||||
func (e EnvVars) init() error {
|
func (e EnvVars) init() error {
|
||||||
var errs []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 {
|
if err := mkDir(e.RuntimeDirPath); err != nil {
|
||||||
errs = append(errs, fmt.Errorf(
|
errs = append(errs, fmt.Errorf(
|
||||||
"creating runtime directory %q: %w",
|
"creating runtime directory %q: %w",
|
||||||
|
47
go/daemon/garage_client_params.go
Normal file
47
go/daemon/garage_client_params.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
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,24 +62,28 @@ func garageInitializeGlobalBucket(
|
|||||||
// putGarageBoostrapHost places the <hostname>.json.signed file for this host
|
// putGarageBoostrapHost places the <hostname>.json.signed file for this host
|
||||||
// into garage so that other hosts are able to see relevant configuration for
|
// into garage so that other hosts are able to see relevant configuration for
|
||||||
// it.
|
// it.
|
||||||
func putGarageBoostrapHost(
|
func (d *daemon) putGarageBoostrapHost(
|
||||||
ctx context.Context, logger *mlog.Logger, currBootstrap bootstrap.Bootstrap,
|
ctx context.Context, logger *mlog.Logger, currBootstrap bootstrap.Bootstrap,
|
||||||
) error {
|
) error {
|
||||||
|
garageClientParams, err := d.getGarageClientParams(ctx, currBootstrap)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting garage client params: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
b = currBootstrap
|
host = currBootstrap.ThisHost()
|
||||||
host = b.ThisHost()
|
client = garageClientParams.GlobalBucketS3APIClient()
|
||||||
client = b.GarageClientParams().GlobalBucketS3APIClient()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
configured, err := nebula.Sign(
|
configured, err := nebula.Sign(
|
||||||
host.HostConfigured, b.PrivateCredentials.SigningPrivateKey,
|
host.HostConfigured, currBootstrap.PrivateCredentials.SigningPrivateKey,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("signing host configured data: %w", err)
|
return fmt.Errorf("signing host configured data: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
hostB, err := json.Marshal(bootstrap.AuthenticatedHost{
|
hostB, err := json.Marshal(bootstrap.AuthenticatedHost{
|
||||||
Assigned: b.SignedHostAssigned,
|
Assigned: currBootstrap.SignedHostAssigned,
|
||||||
Configured: configured,
|
Configured: configured,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -107,14 +111,18 @@ func putGarageBoostrapHost(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getGarageBootstrapHosts(
|
func (d *daemon) getGarageBootstrapHosts(
|
||||||
ctx context.Context, logger *mlog.Logger, currBootstrap bootstrap.Bootstrap,
|
ctx context.Context, logger *mlog.Logger, currBootstrap bootstrap.Bootstrap,
|
||||||
) (
|
) (
|
||||||
map[nebula.HostName]bootstrap.Host, error,
|
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 (
|
var (
|
||||||
b = currBootstrap
|
client = garageClientParams.GlobalBucketS3APIClient()
|
||||||
client = b.GarageClientParams().GlobalBucketS3APIClient()
|
|
||||||
hosts = map[nebula.HostName]bootstrap.Host{}
|
hosts = map[nebula.HostName]bootstrap.Host{}
|
||||||
|
|
||||||
objInfoCh = client.ListObjects(
|
objInfoCh = client.ListObjects(
|
||||||
@ -152,7 +160,7 @@ func getGarageBootstrapHosts(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
host, err := authedHost.Unwrap(b.CAPublicCredentials)
|
host, err := authedHost.Unwrap(currBootstrap.CAPublicCredentials)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn(ctx, "Host could not be authenticated", err)
|
logger.Warn(ctx, "Host could not be authenticated", err)
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,11 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
|
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
|
||||||
@ -38,3 +43,34 @@ func randStr(l int) string {
|
|||||||
}
|
}
|
||||||
return hex.EncodeToString(b)
|
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
|
||||||
|
}
|
||||||
|
@ -87,21 +87,14 @@ func (r *RPC) GetHosts(
|
|||||||
return GetHostsResult{hosts}, nil
|
return GetHostsResult{hosts}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetGarageClientParams returns a GarageClientParams which can be used to
|
// GetGarageClientParams passes the call through to the Daemon method of the
|
||||||
// interact with garage.
|
// same name.
|
||||||
func (r *RPC) GetGarageClientParams(
|
func (r *RPC) GetGarageClientParams(
|
||||||
ctx context.Context, req struct{},
|
ctx context.Context, req struct{},
|
||||||
) (
|
) (
|
||||||
bootstrap.GarageClientParams, error,
|
GarageClientParams, error,
|
||||||
) {
|
) {
|
||||||
b, err := r.daemon.GetBootstrap(ctx)
|
return r.daemon.GetGarageClientParams(ctx)
|
||||||
if err != nil {
|
|
||||||
return bootstrap.GarageClientParams{}, fmt.Errorf(
|
|
||||||
"retrieving bootstrap: %w", err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return b.GarageClientParams(), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNebulaCAPublicCredentials returns the CAPublicCredentials for the network.
|
// GetNebulaCAPublicCredentials returns the CAPublicCredentials for the network.
|
||||||
|
@ -11,6 +11,8 @@ const (
|
|||||||
// accessible to all hosts in the network.
|
// accessible to all hosts in the network.
|
||||||
GlobalBucket = "global-shared"
|
GlobalBucket = "global-shared"
|
||||||
|
|
||||||
|
// GlobalBucketS3APICredentialsName is the main alias of the shared API key
|
||||||
|
// used to write to the global bucket.
|
||||||
GlobalBucketS3APICredentialsName = "global-shared-key"
|
GlobalBucketS3APICredentialsName = "global-shared-key"
|
||||||
|
|
||||||
// ReplicationFactor indicates the replication factor set on the garage
|
// ReplicationFactor indicates the replication factor set on the garage
|
||||||
|
20
go/garage/secrets.go
Normal file
20
go/garage/secrets.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
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,
|
||||||
|
)
|
||||||
|
)
|
14
go/nebula/secrets.go
Normal file
14
go/nebula/secrets.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
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,
|
||||||
|
)
|
13
go/secrets/secrets.go
Normal file
13
go/secrets/secrets.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
// 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))
|
||||||
|
}
|
66
go/secrets/store.go
Normal file
66
go/secrets/store.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
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...)
|
||||||
|
}
|
83
go/secrets/store_fs.go
Normal file
83
go/secrets/store_fs.go
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
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
|
||||||
|
}
|
42
go/secrets/store_fs_test.go
Normal file
42
go/secrets/store_fs_test.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user