diff --git a/go/cmd/entrypoint/daemon.go b/go/cmd/entrypoint/daemon.go index 654aff4..2649974 100644 --- a/go/cmd/entrypoint/daemon.go +++ b/go/cmd/entrypoint/daemon.go @@ -6,6 +6,7 @@ import ( "os" "isle/daemon" + "isle/daemon/daecommon" "dev.mediocregopher.com/mediocre-go-lib.git/mlog" ) @@ -41,7 +42,7 @@ var subCmdDaemon = subCmd{ } if *dumpConfig { - return daemon.CopyDefaultConfig(os.Stdout, envAppDirPath) + return daecommon.CopyDefaultConfig(os.Stdout, envAppDirPath) } logLevel := mlog.LevelFromString(*logLevelStr) @@ -55,7 +56,7 @@ var subCmdDaemon = subCmd{ // required linux capabilities are set. // TODO check that the tun module is loaded (for nebula). - daemonConfig, err := daemon.LoadConfig(envAppDirPath, *daemonConfigPath) + daemonConfig, err := daecommon.LoadConfig(envAppDirPath, *daemonConfigPath) if err != nil { return fmt.Errorf("loading daemon config: %w", err) } diff --git a/go/cmd/entrypoint/main.go b/go/cmd/entrypoint/main.go index 21f89de..6ef40ea 100644 --- a/go/cmd/entrypoint/main.go +++ b/go/cmd/entrypoint/main.go @@ -2,12 +2,13 @@ package main import ( "context" - "isle/daemon" "os" "os/signal" "path/filepath" "syscall" + "isle/daemon/daecommon" + "dev.mediocregopher.com/mediocre-go-lib.git/mctx" "dev.mediocregopher.com/mediocre-go-lib.git/mlog" ) @@ -21,7 +22,7 @@ func getAppDirPath() string { } var ( - daemonEnvVars = daemon.GetEnvVars() + daemonEnvVars = daecommon.GetEnvVars() envAppDirPath = getAppDirPath() envBinDirPath = filepath.Join(envAppDirPath, "bin") ) diff --git a/go/cmd/entrypoint/sub_cmd.go b/go/cmd/entrypoint/sub_cmd.go index 5a260e0..415fe90 100644 --- a/go/cmd/entrypoint/sub_cmd.go +++ b/go/cmd/entrypoint/sub_cmd.go @@ -130,7 +130,9 @@ func (ctx subCmdCtx) doSubCmd(subCmds ...subCmd) error { } daemonRPC := daemon.RPCFromClient( - jsonrpc2.NewUnixHTTPClient(daemon.HTTPSocketPath(), daemonHTTPRPCPath), + jsonrpc2.NewUnixHTTPClient( + daemon.HTTPSocketPath(), daemonHTTPRPCPath, + ), ) err := subCmd.do(subCmdCtx{ diff --git a/go/daemon/bootstrap.go b/go/daemon/bootstrap.go index d8e373a..95a4a01 100644 --- a/go/daemon/bootstrap.go +++ b/go/daemon/bootstrap.go @@ -10,6 +10,7 @@ import ( "reflect" "isle/bootstrap" + "isle/daemon/daecommon" "isle/garage/garagesrv" "isle/jsonutil" "isle/secrets" @@ -42,7 +43,7 @@ func writeBootstrapToStateDir( } func coalesceDaemonConfigAndBootstrap( - daemonConfig Config, hostBootstrap bootstrap.Bootstrap, + daemonConfig daecommon.Config, hostBootstrap bootstrap.Bootstrap, ) ( bootstrap.Bootstrap, error, ) { @@ -88,7 +89,7 @@ type bootstrapDiff struct { } func calcBootstrapDiff( - daemonConfig Config, + daemonConfig daecommon.Config, prevBootstrap, nextBootstrap bootstrap.Bootstrap, ) ( diff bootstrapDiff, err error, diff --git a/go/daemon/child_dnsmasq.go b/go/daemon/child_dnsmasq.go index 4cd361c..7983111 100644 --- a/go/daemon/child_dnsmasq.go +++ b/go/daemon/child_dnsmasq.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "isle/bootstrap" + "isle/daemon/daecommon" "isle/dnsmasq" "path/filepath" "sort" @@ -13,7 +14,7 @@ import ( ) func dnsmasqConfig( - daemonConfig Config, hostBootstrap bootstrap.Bootstrap, + daemonConfig daecommon.Config, hostBootstrap bootstrap.Bootstrap, ) dnsmasq.ConfData { hostsSlice := make([]dnsmasq.ConfDataHost, 0, len(hostBootstrap.Hosts)) for _, host := range hostBootstrap.Hosts { @@ -37,7 +38,7 @@ func dnsmasqConfig( func dnsmasqWriteConfig( runtimeDirPath string, - daemonConfig Config, + daemonConfig daecommon.Config, hostBootstrap bootstrap.Bootstrap, ) ( string, error, @@ -57,7 +58,7 @@ func dnsmasqWriteConfig( func dnsmasqPmuxProcConfig( logger *mlog.Logger, runtimeDirPath, binDirPath string, - daemonConfig Config, + daemonConfig daecommon.Config, hostBootstrap bootstrap.Bootstrap, ) ( pmuxlib.ProcessConfig, error, diff --git a/go/daemon/child_garage.go b/go/daemon/child_garage.go index a636d47..9ef37e6 100644 --- a/go/daemon/child_garage.go +++ b/go/daemon/child_garage.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "isle/bootstrap" + "isle/daemon/daecommon" "isle/garage" "isle/garage/garagesrv" "net" @@ -23,7 +24,7 @@ func garageAdminClientLogger(logger *mlog.Logger) *mlog.Logger { // or it will _panic_ if there is no local instance configured. func newGarageAdminClient( logger *mlog.Logger, - daemonConfig Config, + daemonConfig daecommon.Config, adminToken string, hostBootstrap bootstrap.Bootstrap, ) *garage.AdminClient { @@ -43,7 +44,7 @@ func newGarageAdminClient( func waitForGarage( ctx context.Context, logger *mlog.Logger, - daemonConfig Config, + daemonConfig daecommon.Config, adminToken string, hostBootstrap bootstrap.Bootstrap, ) error { @@ -88,7 +89,7 @@ func waitForGarage( // This assumes that coalesceDaemonConfigAndBootstrap has already been called. func bootstrapGarageHostForAlloc( host bootstrap.Host, - alloc ConfigStorageAllocation, + alloc daecommon.ConfigStorageAllocation, ) bootstrap.GarageHostInstance { for _, inst := range host.Garage.Instances { @@ -103,7 +104,7 @@ func bootstrapGarageHostForAlloc( func garageWriteChildConfig( rpcSecret, runtimeDirPath, adminToken string, hostBootstrap bootstrap.Bootstrap, - alloc ConfigStorageAllocation, + alloc daecommon.ConfigStorageAllocation, ) ( string, error, ) { @@ -147,7 +148,7 @@ func garagePmuxProcConfigs( ctx context.Context, logger *mlog.Logger, rpcSecret, runtimeDirPath, binDirPath string, - daemonConfig Config, + daemonConfig daecommon.Config, adminToken string, hostBootstrap bootstrap.Bootstrap, ) ( @@ -188,7 +189,7 @@ func garagePmuxProcConfigs( func garageApplyLayout( ctx context.Context, logger *mlog.Logger, - daemonConfig Config, + daemonConfig daecommon.Config, adminToken string, hostBootstrap bootstrap.Bootstrap, ) error { diff --git a/go/daemon/child_nebula.go b/go/daemon/child_nebula.go index cdab02d..875c59c 100644 --- a/go/daemon/child_nebula.go +++ b/go/daemon/child_nebula.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "isle/bootstrap" + "isle/daemon/daecommon" "isle/yamlutil" "net" "path/filepath" @@ -47,7 +48,7 @@ func waitForNebula( } func nebulaConfig( - daemonConfig Config, + daemonConfig daecommon.Config, hostBootstrap bootstrap.Bootstrap, ) ( map[string]any, error, @@ -136,7 +137,7 @@ func nebulaConfig( func nebulaWriteConfig( runtimeDirPath string, - daemonConfig Config, + daemonConfig daecommon.Config, hostBootstrap bootstrap.Bootstrap, ) ( string, error, @@ -157,7 +158,7 @@ func nebulaWriteConfig( func nebulaPmuxProcConfig( runtimeDirPath, binDirPath string, - daemonConfig Config, + daemonConfig daecommon.Config, hostBootstrap bootstrap.Bootstrap, ) ( pmuxlib.ProcessConfig, error, diff --git a/go/daemon/child_pmux.go b/go/daemon/child_pmux.go index 9be0946..1592350 100644 --- a/go/daemon/child_pmux.go +++ b/go/daemon/child_pmux.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "isle/bootstrap" + "isle/daemon/daecommon" "code.betamike.com/micropelago/pmux/pmuxlib" ) @@ -11,7 +12,7 @@ import ( func (c *Children) newPmuxConfig( ctx context.Context, garageRPCSecret, binDirPath string, - daemonConfig Config, + daemonConfig daecommon.Config, garageAdminToken string, hostBootstrap bootstrap.Bootstrap, ) ( @@ -67,7 +68,7 @@ func (c *Children) newPmuxConfig( func (c *Children) postPmuxInit( ctx context.Context, - daemonConfig Config, + daemonConfig daecommon.Config, garageAdminToken string, hostBootstrap bootstrap.Bootstrap, ) error { diff --git a/go/daemon/children.go b/go/daemon/children.go index e1de983..024a950 100644 --- a/go/daemon/children.go +++ b/go/daemon/children.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "isle/bootstrap" + "isle/daemon/daecommon" "isle/secrets" "code.betamike.com/micropelago/pmux/pmuxlib" @@ -18,7 +19,7 @@ import ( // - garage (0 or more, depending on configured storage allocations) type Children struct { logger *mlog.Logger - daemonConfig Config + daemonConfig daecommon.Config opts Opts pmux *pmuxlib.Pmux @@ -31,7 +32,7 @@ func NewChildren( logger *mlog.Logger, binDirPath string, secretsStore secrets.Store, - daemonConfig Config, + daemonConfig daecommon.Config, garageAdminToken string, hostBootstrap bootstrap.Bootstrap, opts *Opts, @@ -41,7 +42,7 @@ func NewChildren( opts = opts.withDefaults() logger.Info(ctx, "Loading secrets") - garageRPCSecret, err := getGarageRPCSecret(ctx, secretsStore) + garageRPCSecret, err := daecommon.GetGarageRPCSecret(ctx, secretsStore) if err != nil && !errors.Is(err, secrets.ErrNotFound) { return nil, fmt.Errorf("loading garage RPC secret: %w", err) } diff --git a/go/daemon/config.go b/go/daemon/config.go index 8f49cac..61f171c 100644 --- a/go/daemon/config.go +++ b/go/daemon/config.go @@ -1,183 +1,81 @@ package daemon import ( + "errors" "fmt" - "io" - "isle/yamlutil" + "io/fs" "os" "path/filepath" - "strconv" - - "github.com/imdario/mergo" - "gopkg.in/yaml.v3" + "slices" + "strings" + "sync" ) -func defaultConfigPath(appDirPath string) string { - return filepath.Join(appDirPath, "etc", "daemon.yml") -} +func getDefaultHTTPSocketDirPath() string { + path, err := firstExistingDir( + "/tmp", -type ConfigTun struct { - Device string `yaml:"device"` -} - -type ConfigFirewall struct { - Conntrack ConfigConntrack `yaml:"conntrack"` - Outbound []ConfigFirewallRule `yaml:"outbound"` - Inbound []ConfigFirewallRule `yaml:"inbound"` -} - -type ConfigConntrack struct { - TCPTimeout string `yaml:"tcp_timeout"` - UDPTimeout string `yaml:"udp_timeout"` - DefaultTimeout string `yaml:"default_timeout"` - MaxConnections int `yaml:"max_connections"` -} - -type ConfigFirewallRule struct { - Port string `yaml:"port,omitempty"` - Code string `yaml:"code,omitempty"` - Proto string `yaml:"proto,omitempty"` - Host string `yaml:"host,omitempty"` - Group string `yaml:"group,omitempty"` - Groups []string `yaml:"groups,omitempty"` - CIDR string `yaml:"cidr,omitempty"` - CASha string `yaml:"ca_sha,omitempty"` - CAName string `yaml:"ca_name,omitempty"` -} - -// ConfigStorageAllocation describes the structure of each storage allocation -// within the daemon config file. -type ConfigStorageAllocation struct { - DataPath string `yaml:"data_path"` - MetaPath string `yaml:"meta_path"` - Capacity int `yaml:"capacity"` - S3APIPort int `yaml:"s3_api_port"` - RPCPort int `yaml:"rpc_port"` - AdminPort int `yaml:"admin_port"` - - // Zone is a secret option which makes it easier to test garage bugs, but - // which we don't want users to otherwise know about. - Zone string `yaml:"zone"` -} - -// Config describes the structure of the daemon config file. -type Config struct { - DNS struct { - Resolvers []string `yaml:"resolvers"` - } `yaml:"dns"` - VPN struct { - PublicAddr string `yaml:"public_addr"` - Firewall ConfigFirewall `yaml:"firewall"` - Tun ConfigTun `yaml:"tun"` - } `yaml:"vpn"` - Storage struct { - Allocations []ConfigStorageAllocation - } `yaml:"storage"` -} - -func (c *Config) fillDefaults() { - - var firewallGarageInbound []ConfigFirewallRule - - for i := range c.Storage.Allocations { - if c.Storage.Allocations[i].RPCPort == 0 { - c.Storage.Allocations[i].RPCPort = 3900 + (i * 10) - } - - if c.Storage.Allocations[i].S3APIPort == 0 { - c.Storage.Allocations[i].S3APIPort = 3901 + (i * 10) - } - - if c.Storage.Allocations[i].AdminPort == 0 { - c.Storage.Allocations[i].AdminPort = 3902 + (i * 10) - } - - alloc := c.Storage.Allocations[i] - - firewallGarageInbound = append( - firewallGarageInbound, - ConfigFirewallRule{ - Port: strconv.Itoa(alloc.S3APIPort), - Proto: "tcp", - Host: "any", - }, - ConfigFirewallRule{ - Port: strconv.Itoa(alloc.RPCPort), - Proto: "tcp", - Host: "any", - }, - ) - } - - c.VPN.Firewall.Inbound = append( - c.VPN.Firewall.Inbound, - firewallGarageInbound..., + // TODO it's possible the daemon process can't actually write to these + "/run", + "/var/run", + "/dev/shm", ) -} - -// CopyDefaultConfig copies the daemon config file embedded in the AppDir into -// the given io.Writer. -func CopyDefaultConfig(into io.Writer, appDirPath string) error { - - defaultConfigPath := defaultConfigPath(appDirPath) - - f, err := os.Open(defaultConfigPath) if err != nil { - return fmt.Errorf("opening daemon config at %q: %w", defaultConfigPath, err) + panic(fmt.Sprintf("Failed to find directory for HTTP socket: %v", err)) } - defer f.Close() - - if _, err := io.Copy(into, f); err != nil { - return fmt.Errorf("copying daemon config from %q: %w", defaultConfigPath, err) - } - - return nil + return path } -// LoadConfig loads the daemon config from userConfigPath, merges it with -// the default found in the appDirPath, and returns the result. -// -// If userConfigPath is not given then the default is loaded and returned. -func LoadConfig( - appDirPath, userConfigPath string, -) ( - Config, error, -) { +// HTTPSocketPath returns the path to the daemon's HTTP socket which is used for +// RPC and other functionality. +var HTTPSocketPath = sync.OnceValue(func() string { + return envOr( + "ISLE_DAEMON_HTTP_SOCKET_PATH", + func() string { + return filepath.Join( + getDefaultHTTPSocketDirPath(), "isle-daemon.sock", + ) + }, + ) +}) - defaultConfigPath := defaultConfigPath(appDirPath) +//////////////////////////////////////////////////////////////////////////////// +// Jigs - var fullDaemon map[string]interface{} - - if err := yamlutil.LoadYamlFile(&fullDaemon, defaultConfigPath); err != nil { - return Config{}, fmt.Errorf("parsing default daemon config file: %w", err) +func envOr(name string, fallback func() string) string { + if v := os.Getenv(name); v != "" { + return v } + return fallback() +} - if userConfigPath != "" { - - var daemonConfig map[string]interface{} - if err := yamlutil.LoadYamlFile(&daemonConfig, userConfigPath); err != nil { - return Config{}, fmt.Errorf("parsing %q: %w", userConfigPath, err) - } - - err := mergo.Merge(&fullDaemon, daemonConfig, mergo.WithOverride) - if err != nil { - return Config{}, fmt.Errorf("merging contents of file %q: %w", userConfigPath, err) +func firstExistingDir(paths ...string) (string, error) { + var errs []error + for _, path := range paths { + stat, err := os.Stat(path) + switch { + case errors.Is(err, fs.ErrExist): + continue + case err != nil: + errs = append( + errs, fmt.Errorf("checking if path %q exists: %w", path, err), + ) + case !stat.IsDir(): + errs = append( + errs, fmt.Errorf("path %q exists but is not a directory", path), + ) + default: + return path, nil } } - fullDaemonB, err := yaml.Marshal(fullDaemon) - - if err != nil { - return Config{}, fmt.Errorf("yaml marshaling: %w", err) + err := fmt.Errorf( + "no directory found at any of the following paths: %s", + strings.Join(paths, ", "), + ) + if len(errs) > 0 { + err = errors.Join(slices.Insert(errs, 0, err)...) } - - var config Config - if err := yaml.Unmarshal(fullDaemonB, &config); err != nil { - return Config{}, fmt.Errorf("yaml unmarshaling back into Config struct: %w", err) - } - - config.fillDefaults() - - return config, nil + return "", err } diff --git a/go/daemon/daecommon/config.go b/go/daemon/daecommon/config.go new file mode 100644 index 0000000..cd06397 --- /dev/null +++ b/go/daemon/daecommon/config.go @@ -0,0 +1,183 @@ +package daecommon + +import ( + "fmt" + "io" + "isle/yamlutil" + "os" + "path/filepath" + "strconv" + + "github.com/imdario/mergo" + "gopkg.in/yaml.v3" +) + +func defaultConfigPath(appDirPath string) string { + return filepath.Join(appDirPath, "etc", "daemon.yml") +} + +type ConfigTun struct { + Device string `yaml:"device"` +} + +type ConfigFirewall struct { + Conntrack ConfigConntrack `yaml:"conntrack"` + Outbound []ConfigFirewallRule `yaml:"outbound"` + Inbound []ConfigFirewallRule `yaml:"inbound"` +} + +type ConfigConntrack struct { + TCPTimeout string `yaml:"tcp_timeout"` + UDPTimeout string `yaml:"udp_timeout"` + DefaultTimeout string `yaml:"default_timeout"` + MaxConnections int `yaml:"max_connections"` +} + +type ConfigFirewallRule struct { + Port string `yaml:"port,omitempty"` + Code string `yaml:"code,omitempty"` + Proto string `yaml:"proto,omitempty"` + Host string `yaml:"host,omitempty"` + Group string `yaml:"group,omitempty"` + Groups []string `yaml:"groups,omitempty"` + CIDR string `yaml:"cidr,omitempty"` + CASha string `yaml:"ca_sha,omitempty"` + CAName string `yaml:"ca_name,omitempty"` +} + +// ConfigStorageAllocation describes the structure of each storage allocation +// within the daemon config file. +type ConfigStorageAllocation struct { + DataPath string `yaml:"data_path"` + MetaPath string `yaml:"meta_path"` + Capacity int `yaml:"capacity"` + S3APIPort int `yaml:"s3_api_port"` + RPCPort int `yaml:"rpc_port"` + AdminPort int `yaml:"admin_port"` + + // Zone is a secret option which makes it easier to test garage bugs, but + // which we don't want users to otherwise know about. + Zone string `yaml:"zone"` +} + +// Config describes the structure of the daemon config file. +type Config struct { + DNS struct { + Resolvers []string `yaml:"resolvers"` + } `yaml:"dns"` + VPN struct { + PublicAddr string `yaml:"public_addr"` + Firewall ConfigFirewall `yaml:"firewall"` + Tun ConfigTun `yaml:"tun"` + } `yaml:"vpn"` + Storage struct { + Allocations []ConfigStorageAllocation + } `yaml:"storage"` +} + +func (c *Config) fillDefaults() { + + var firewallGarageInbound []ConfigFirewallRule + + for i := range c.Storage.Allocations { + if c.Storage.Allocations[i].RPCPort == 0 { + c.Storage.Allocations[i].RPCPort = 3900 + (i * 10) + } + + if c.Storage.Allocations[i].S3APIPort == 0 { + c.Storage.Allocations[i].S3APIPort = 3901 + (i * 10) + } + + if c.Storage.Allocations[i].AdminPort == 0 { + c.Storage.Allocations[i].AdminPort = 3902 + (i * 10) + } + + alloc := c.Storage.Allocations[i] + + firewallGarageInbound = append( + firewallGarageInbound, + ConfigFirewallRule{ + Port: strconv.Itoa(alloc.S3APIPort), + Proto: "tcp", + Host: "any", + }, + ConfigFirewallRule{ + Port: strconv.Itoa(alloc.RPCPort), + Proto: "tcp", + Host: "any", + }, + ) + } + + c.VPN.Firewall.Inbound = append( + c.VPN.Firewall.Inbound, + firewallGarageInbound..., + ) +} + +// CopyDefaultConfig copies the daemon config file embedded in the AppDir into +// the given io.Writer. +func CopyDefaultConfig(into io.Writer, appDirPath string) error { + + defaultConfigPath := defaultConfigPath(appDirPath) + + f, err := os.Open(defaultConfigPath) + if err != nil { + return fmt.Errorf("opening daemon config at %q: %w", defaultConfigPath, err) + } + + defer f.Close() + + if _, err := io.Copy(into, f); err != nil { + return fmt.Errorf("copying daemon config from %q: %w", defaultConfigPath, err) + } + + return nil +} + +// LoadConfig loads the daemon config from userConfigPath, merges it with +// the default found in the appDirPath, and returns the result. +// +// If userConfigPath is not given then the default is loaded and returned. +func LoadConfig( + appDirPath, userConfigPath string, +) ( + Config, error, +) { + + defaultConfigPath := defaultConfigPath(appDirPath) + + var fullDaemon map[string]interface{} + + if err := yamlutil.LoadYamlFile(&fullDaemon, defaultConfigPath); err != nil { + return Config{}, fmt.Errorf("parsing default daemon config file: %w", err) + } + + if userConfigPath != "" { + + var daemonConfig map[string]interface{} + if err := yamlutil.LoadYamlFile(&daemonConfig, userConfigPath); err != nil { + return Config{}, fmt.Errorf("parsing %q: %w", userConfigPath, err) + } + + err := mergo.Merge(&fullDaemon, daemonConfig, mergo.WithOverride) + if err != nil { + return Config{}, fmt.Errorf("merging contents of file %q: %w", userConfigPath, err) + } + } + + fullDaemonB, err := yaml.Marshal(fullDaemon) + + if err != nil { + return Config{}, fmt.Errorf("yaml marshaling: %w", err) + } + + var config Config + if err := yaml.Unmarshal(fullDaemonB, &config); err != nil { + return Config{}, fmt.Errorf("yaml unmarshaling back into Config struct: %w", err) + } + + config.fillDefaults() + + return config, nil +} diff --git a/go/daemon/daecommon/daecommon.go b/go/daemon/daecommon/daecommon.go new file mode 100644 index 0000000..38e2349 --- /dev/null +++ b/go/daemon/daecommon/daecommon.go @@ -0,0 +1,3 @@ +// Package daecommon holds types and functionality which are required the daemon +// package and other of its subpackages. +package daecommon diff --git a/go/daemon/daecommon/env.go b/go/daemon/daecommon/env.go new file mode 100644 index 0000000..d1ee66e --- /dev/null +++ b/go/daemon/daecommon/env.go @@ -0,0 +1,45 @@ +package daecommon + +import ( + "os" + "path/filepath" + "sync" + + "github.com/adrg/xdg" +) + +// EnvVars are variables which are derived based on the environment which the +// process is running in. +type EnvVars struct { + RuntimeDirPath string + StateDirPath string +} + +// GetEnvVars will return the EnvVars of the current processes, as determined by +// the process's environment. +var GetEnvVars = sync.OnceValue(func() (v EnvVars) { + // RUNTIME_DIRECTORY/STATE_DIRECTORY are used by the systemd service in + // conjunction with the RuntimeDirectory/StateDirectory directives. + + v.RuntimeDirPath = envOr( + "RUNTIME_DIRECTORY", + func() string { return filepath.Join(xdg.RuntimeDir, "isle") }, + ) + + v.StateDirPath = envOr( + "STATE_DIRECTORY", + func() string { return filepath.Join(xdg.StateHome, "isle") }, + ) + + return +}) + +//////////////////////////////////////////////////////////////////////////////// +// Jigs + +func envOr(name string, fallback func() string) string { + if v := os.Getenv(name); v != "" { + return v + } + return fallback() +} diff --git a/go/daemon/daecommon/secrets.go b/go/daemon/daecommon/secrets.go new file mode 100644 index 0000000..622d5c8 --- /dev/null +++ b/go/daemon/daecommon/secrets.go @@ -0,0 +1,51 @@ +package daecommon + +import ( + "fmt" + "isle/garage" + "isle/nebula" + "isle/secrets" +) + +const ( + secretsNSNebula = "nebula" + secretsNSGarage = "garage" +) + +//////////////////////////////////////////////////////////////////////////////// +// Nebula-related secrets + +// IDs and Get/Set functions for nebula-related secrets. +var ( + NebulaCASigningPrivateKeySecretID = secrets.NewID(secretsNSNebula, "ca-signing-private-key") + + GetNebulaCASigningPrivateKey, SetNebulaCASigningPrivateKey = secrets.GetSetFunctions[nebula.SigningPrivateKey]( + NebulaCASigningPrivateKeySecretID, + ) +) + +//////////////////////////////////////////////////////////////////////////////// +// Garage-related secrets + +func garageS3APIBucketCredentialsSecretID(credsName string) secrets.ID { + return secrets.NewID( + secretsNSGarage, fmt.Sprintf("s3-api-bucket-credentials-%s", credsName), + ) +} + +// IDs and Get/Set functions for garage-related secrets. +var ( + GarageRPCSecretSecretID = secrets.NewID(secretsNSGarage, "rpc-secret") + GarageS3APIGlobalBucketCredentialsSecretID = garageS3APIBucketCredentialsSecretID( + garage.GlobalBucketS3APICredentialsName, + ) + + GetGarageRPCSecret, SetGarageRPCSecret = secrets.GetSetFunctions[string]( + GarageRPCSecretSecretID, + ) + + GetGarageS3APIGlobalBucketCredentials, + SetGarageS3APIGlobalBucketCredentials = secrets.GetSetFunctions[garage.S3APICredentials]( + GarageS3APIGlobalBucketCredentialsSecretID, + ) +) diff --git a/go/daemon/daemon.go b/go/daemon/daemon.go index 1e01f92..e60a698 100644 --- a/go/daemon/daemon.go +++ b/go/daemon/daemon.go @@ -12,6 +12,7 @@ import ( "io" "io/fs" "isle/bootstrap" + "isle/daemon/daecommon" "isle/jsonutil" "isle/nebula" "isle/secrets" @@ -33,7 +34,8 @@ type Opts struct { // will be directed to. Stdout, Stderr io.Writer - EnvVars EnvVars // Defaults to that returned by GetEnvVars. + // Defaults to that returned by daecommon.GetEnvVars. + EnvVars daecommon.EnvVars } func (o *Opts) withDefaults() *Opts { @@ -49,8 +51,8 @@ func (o *Opts) withDefaults() *Opts { o.Stderr = os.Stderr } - if o.EnvVars == (EnvVars{}) { - o.EnvVars = GetEnvVars() + if o.EnvVars == (daecommon.EnvVars{}) { + o.EnvVars = daecommon.GetEnvVars() } return o @@ -83,7 +85,7 @@ var _ RPC = (*Daemon)(nil) // canceled. type Daemon struct { logger *mlog.Logger - daemonConfig Config + daemonConfig daecommon.Config envBinDirPath string opts *Opts @@ -103,7 +105,7 @@ type Daemon struct { func NewDaemon( ctx context.Context, logger *mlog.Logger, - daemonConfig Config, + daemonConfig daecommon.Config, envBinDirPath string, opts *Opts, ) ( @@ -121,7 +123,7 @@ func NewDaemon( bootstrapFilePath = bootstrap.StateDirPath(d.opts.EnvVars.StateDirPath) ) - if err := d.opts.EnvVars.init(); err != nil { + if err := createDirs(d.opts.EnvVars); err != nil { return nil, fmt.Errorf("initializing daemon directories: %w", err) } @@ -151,6 +153,27 @@ func NewDaemon( return d, nil } +func createDirs(e daecommon.EnvVars) error { + var errs []error + if err := mkDir(e.RuntimeDirPath); err != nil { + errs = append(errs, fmt.Errorf( + "creating runtime directory %q: %w", + e.RuntimeDirPath, + err, + )) + } + + if err := mkDir(e.StateDirPath); err != nil { + errs = append(errs, fmt.Errorf( + "creating state directory %q: %w", + e.StateDirPath, + err, + )) + } + + return errors.Join(errs...) +} + // initialize must be called with d.l write lock held. func (d *Daemon) initialize( ctx context.Context, currBootstrap bootstrap.Bootstrap, @@ -312,7 +335,9 @@ func (d *Daemon) postInit(ctx context.Context) error { // // TODO this is pretty hacky, but there doesn't seem to be a better way to // manage it at the moment. - _, err := getGarageS3APIGlobalBucketCredentials(ctx, d.secretsStore) + _, err := daecommon.GetGarageS3APIGlobalBucketCredentials( + ctx, d.secretsStore, + ) if errors.Is(err, secrets.ErrNotFound) { d.logger.Info(ctx, "Initializing garage shared global bucket") garageGlobalBucketCreds, err := garageInitializeGlobalBucket( @@ -326,7 +351,7 @@ func (d *Daemon) postInit(ctx context.Context) error { return fmt.Errorf("initializing global bucket: %w", err) } - err = setGarageS3APIGlobalBucketCredentials( + err = daecommon.SetGarageS3APIGlobalBucketCredentials( ctx, d.secretsStore, garageGlobalBucketCreds, ) if err != nil { @@ -406,12 +431,14 @@ func (d *Daemon) CreateNetwork( garageRPCSecret = randStr(32) ) - err = setGarageRPCSecret(ctx, d.secretsStore, garageRPCSecret) + err = daecommon.SetGarageRPCSecret(ctx, d.secretsStore, garageRPCSecret) if err != nil { return fmt.Errorf("setting garage RPC secret: %w", err) } - err = setNebulaCASigningPrivateKey(ctx, d.secretsStore, nebulaCACreds.SigningPrivateKey) + err = daecommon.SetNebulaCASigningPrivateKey( + ctx, d.secretsStore, nebulaCACreds.SigningPrivateKey, + ) if err != nil { return fmt.Errorf("setting nebula CA signing key secret: %w", err) } @@ -679,7 +706,7 @@ func (d *Daemon) CreateHost( } // TODO if the ip is given, check that it's not already in use. - caSigningPrivateKey, err := getNebulaCASigningPrivateKey( + caSigningPrivateKey, err := daecommon.GetNebulaCASigningPrivateKey( ctx, d.secretsStore, ) if err != nil { @@ -701,12 +728,14 @@ func (d *Daemon) CreateHost( } secretsIDs := []secrets.ID{ - garageRPCSecretSecretID, - garageS3APIGlobalBucketCredentialsSecretID, + daecommon.GarageRPCSecretSecretID, + daecommon.GarageS3APIGlobalBucketCredentialsSecretID, } if opts.CanCreateHosts { - secretsIDs = append(secretsIDs, nebulaCASigningPrivateKeySecretID) + secretsIDs = append( + secretsIDs, daecommon.NebulaCASigningPrivateKeySecretID, + ) } if joiningBootstrap.Secrets, err = secrets.Export( @@ -760,7 +789,7 @@ func (d *Daemon) CreateNebulaCertificate( } ip := host.IP() - caSigningPrivateKey, err := getNebulaCASigningPrivateKey( + caSigningPrivateKey, err := daecommon.GetNebulaCASigningPrivateKey( ctx, d.secretsStore, ) if err != nil { diff --git a/go/daemon/env.go b/go/daemon/env.go deleted file mode 100644 index 29c795e..0000000 --- a/go/daemon/env.go +++ /dev/null @@ -1,130 +0,0 @@ -package daemon - -import ( - "errors" - "fmt" - "io/fs" - "os" - "path/filepath" - "slices" - "strings" - "sync" - - "github.com/adrg/xdg" -) - -// EnvVars are variables which are derived based on the environment which the -// process is running in. -type EnvVars struct { - RuntimeDirPath string - StateDirPath string -} - -func (e EnvVars) init() error { - var errs []error - if err := mkDir(e.RuntimeDirPath); err != nil { - errs = append(errs, fmt.Errorf( - "creating runtime directory %q: %w", - e.RuntimeDirPath, - err, - )) - } - - if err := mkDir(e.StateDirPath); err != nil { - errs = append(errs, fmt.Errorf( - "creating state directory %q: %w", - e.StateDirPath, - err, - )) - } - - return errors.Join(errs...) -} - -func getDefaultHTTPSocketDirPath() string { - path, err := firstExistingDir( - "/tmp", - - // TODO it's possible the daemon process can't actually write to these - "/run", - "/var/run", - "/dev/shm", - ) - if err != nil { - panic(fmt.Sprintf("Failed to find directory for HTTP socket: %v", err)) - } - - return path -} - -// HTTPSocketPath returns the path to the daemon's HTTP socket which is used for -// RPC and other functionality. -var HTTPSocketPath = sync.OnceValue(func() string { - return envOr( - "ISLE_DAEMON_HTTP_SOCKET_PATH", - func() string { - return filepath.Join( - getDefaultHTTPSocketDirPath(), "isle-daemon.sock", - ) - }, - ) -}) - -// GetEnvVars will return the EnvVars of the current processes, as determined by -// the process's environment. -var GetEnvVars = sync.OnceValue(func() (v EnvVars) { - // RUNTIME_DIRECTORY/STATE_DIRECTORY are used by the systemd service in - // conjunction with the RuntimeDirectory/StateDirectory directives. - - v.RuntimeDirPath = envOr( - "RUNTIME_DIRECTORY", - func() string { return filepath.Join(xdg.RuntimeDir, "isle") }, - ) - - v.StateDirPath = envOr( - "STATE_DIRECTORY", - func() string { return filepath.Join(xdg.StateHome, "isle") }, - ) - - return -}) - -//////////////////////////////////////////////////////////////////////////////// -// Jigs - -func envOr(name string, fallback func() string) string { - if v := os.Getenv(name); v != "" { - return v - } - return fallback() -} - -func firstExistingDir(paths ...string) (string, error) { - var errs []error - for _, path := range paths { - stat, err := os.Stat(path) - switch { - case errors.Is(err, fs.ErrExist): - continue - case err != nil: - errs = append( - errs, fmt.Errorf("checking if path %q exists: %w", path, err), - ) - case !stat.IsDir(): - errs = append( - errs, fmt.Errorf("path %q exists but is not a directory", path), - ) - default: - return path, nil - } - } - - err := fmt.Errorf( - "no directory found at any of the following paths: %s", - strings.Join(paths, ", "), - ) - if len(errs) > 0 { - err = errors.Join(slices.Insert(errs, 0, err)...) - } - return "", err -} diff --git a/go/daemon/garage_client_params.go b/go/daemon/garage_client_params.go index 8bc24fc..416f7d1 100644 --- a/go/daemon/garage_client_params.go +++ b/go/daemon/garage_client_params.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "isle/bootstrap" + "isle/daemon/daecommon" "isle/garage" "isle/secrets" ) @@ -24,12 +25,14 @@ func (d *Daemon) getGarageClientParams( ) ( GarageClientParams, error, ) { - creds, err := getGarageS3APIGlobalBucketCredentials(ctx, d.secretsStore) + creds, err := daecommon.GetGarageS3APIGlobalBucketCredentials( + ctx, d.secretsStore, + ) if err != nil { return GarageClientParams{}, fmt.Errorf("getting garage global bucket creds: %w", err) } - rpcSecret, err := getGarageRPCSecret(ctx, d.secretsStore) + rpcSecret, err := daecommon.GetGarageRPCSecret(ctx, d.secretsStore) if err != nil && !errors.Is(err, secrets.ErrNotFound) { return GarageClientParams{}, fmt.Errorf("getting garage rpc secret: %w", err) } diff --git a/go/daemon/global_bucket.go b/go/daemon/global_bucket.go index 95e1297..53914e3 100644 --- a/go/daemon/global_bucket.go +++ b/go/daemon/global_bucket.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "isle/bootstrap" + "isle/daemon/daecommon" "isle/garage" "isle/nebula" "path/filepath" @@ -23,7 +24,7 @@ const ( func garageInitializeGlobalBucket( ctx context.Context, logger *mlog.Logger, - daemonConfig Config, + daemonConfig daecommon.Config, adminToken string, hostBootstrap bootstrap.Bootstrap, ) ( diff --git a/go/daemon/secrets.go b/go/daemon/secrets.go deleted file mode 100644 index c68f1ad..0000000 --- a/go/daemon/secrets.go +++ /dev/null @@ -1,52 +0,0 @@ -package daemon - -import ( - "fmt" - "isle/garage" - "isle/nebula" - "isle/secrets" -) - -const ( - secretsNSNebula = "nebula" - secretsNSGarage = "garage" -) - -//////////////////////////////////////////////////////////////////////////////// -// Nebula-related secrets - -var ( - nebulaCASigningPrivateKeySecretID = secrets.NewID(secretsNSNebula, "ca-signing-private-key") -) - -var getNebulaCASigningPrivateKey, setNebulaCASigningPrivateKey = secrets.GetSetFunctions[nebula.SigningPrivateKey]( - nebulaCASigningPrivateKeySecretID, -) - -//////////////////////////////////////////////////////////////////////////////// -// 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, - ) -)