diff --git a/go/bootstrap/bootstrap.go b/go/bootstrap/bootstrap.go index fe48fa6..dc3b1a6 100644 --- a/go/bootstrap/bootstrap.go +++ b/go/bootstrap/bootstrap.go @@ -93,12 +93,18 @@ func New( }, nil } -// FromReader reads a bootstrap file from the given io.Reader. -func FromReader(r io.Reader) (Bootstrap, error) { +// 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 - err := json.NewDecoder(r).Decode(&b) - if err != nil { + if err := json.NewDecoder(f).Decode(&b); err != nil { return Bootstrap{}, fmt.Errorf("decoding json: %w", err) } @@ -106,19 +112,7 @@ func FromReader(r io.Reader) (Bootstrap, error) { return Bootstrap{}, fmt.Errorf("unwrapping host assigned: %w", err) } - return b, err -} - -// FromFile reads a bootstrap from a file at the given path. -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() - - return FromReader(f) + return b, nil } // WriteTo writes the Bootstrap as a new bootstrap to the given io.Writer. diff --git a/go/cmd/entrypoint/admin.go b/go/cmd/entrypoint/admin.go index 711105f..f522070 100644 --- a/go/cmd/entrypoint/admin.go +++ b/go/cmd/entrypoint/admin.go @@ -171,9 +171,7 @@ var subCmdAdminCreateNetwork = subCmd{ logger.WithNamespace("daemon"), daemonConfig, hostBootstrap, - envRuntimeDirPath, envBinDirPath, - envStateDirPath, &daemon.Opts{ // SkipHostBootstrapPush is required, because the global bucket // hasn't actually been initialized yet, so there's nowhere to diff --git a/go/cmd/entrypoint/bootstrap_util.go b/go/cmd/entrypoint/bootstrap_util.go index 0307214..b55ac8c 100644 --- a/go/cmd/entrypoint/bootstrap_util.go +++ b/go/cmd/entrypoint/bootstrap_util.go @@ -11,7 +11,7 @@ import ( func loadHostBootstrap() (bootstrap.Bootstrap, error) { - stateDirPath := bootstrap.StateDirPath(envStateDirPath) + stateDirPath := bootstrap.StateDirPath(daemonEnvVars.StateDirPath) hostBootstrap, err := bootstrap.FromFile(stateDirPath) if errors.Is(err, fs.ErrNotExist) { @@ -29,7 +29,7 @@ func loadHostBootstrap() (bootstrap.Bootstrap, error) { func writeBootstrapToStateDir(hostBootstrap bootstrap.Bootstrap) error { - path := bootstrap.StateDirPath(envStateDirPath) + path := bootstrap.StateDirPath(daemonEnvVars.StateDirPath) dirPath := filepath.Dir(path) if err := os.MkdirAll(dirPath, 0700); err != nil { diff --git a/go/cmd/entrypoint/daemon.go b/go/cmd/entrypoint/daemon.go index 2567fc2..db5253b 100644 --- a/go/cmd/entrypoint/daemon.go +++ b/go/cmd/entrypoint/daemon.go @@ -98,9 +98,7 @@ func runDaemonPmuxOnce( logger.WithNamespace("daemon"), daemonConfig, hostBootstrap, - envRuntimeDirPath, envBinDirPath, - envStateDirPath, nil, ) if err != nil { @@ -214,7 +212,7 @@ var subCmdDaemon = subCmd{ defer runtimeDirCleanup() var ( - bootstrapStateDirPath = bootstrap.StateDirPath(envStateDirPath) + bootstrapStateDirPath = bootstrap.StateDirPath(daemonEnvVars.StateDirPath) bootstrapAppDirPath = bootstrap.AppDirPath(envAppDirPath) hostBootstrapPath string diff --git a/go/cmd/entrypoint/daemon_util.go b/go/cmd/entrypoint/daemon_util.go index d64b8cc..8323c67 100644 --- a/go/cmd/entrypoint/daemon_util.go +++ b/go/cmd/entrypoint/daemon_util.go @@ -66,10 +66,12 @@ func newHTTPServer( ) ( *http.Server, error, ) { - l, err := net.Listen("unix", envSocketPath) + l, err := net.Listen("unix", daemonEnvVars.HTTPSocketPath) if err != nil { return nil, fmt.Errorf( - "failed to listen on socket %q: %w", envSocketPath, err, + "failed to listen on socket %q: %w", + daemonEnvVars.HTTPSocketPath, + err, ) } diff --git a/go/cmd/entrypoint/garage.go b/go/cmd/entrypoint/garage.go index b17bd32..d8573c8 100644 --- a/go/cmd/entrypoint/garage.go +++ b/go/cmd/entrypoint/garage.go @@ -13,7 +13,7 @@ import ( // order to prevent it from doing so. func initMCConfigDir() (string, error) { var ( - path = filepath.Join(envStateDirPath, "mc") + path = filepath.Join(daemonEnvVars.StateDirPath, "mc") sharePath = filepath.Join(path, "share") configJSONPath = filepath.Join(path, "config.json") ) diff --git a/go/cmd/entrypoint/jigs.go b/go/cmd/entrypoint/jigs.go deleted file mode 100644 index b8ba318..0000000 --- a/go/cmd/entrypoint/jigs.go +++ /dev/null @@ -1,47 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "io/fs" - "os" - "slices" - "strings" -) - -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/cmd/entrypoint/main.go b/go/cmd/entrypoint/main.go index 88d0f2a..5c509c3 100644 --- a/go/cmd/entrypoint/main.go +++ b/go/cmd/entrypoint/main.go @@ -2,7 +2,7 @@ package main import ( "context" - "fmt" + "isle/daemon" "os" "os/signal" "path/filepath" @@ -10,14 +10,8 @@ import ( "dev.mediocregopher.com/mediocre-go-lib.git/mctx" "dev.mediocregopher.com/mediocre-go-lib.git/mlog" - "github.com/adrg/xdg" ) -// The purpose of this binary is to act as the entrypoint of the isle -// process. It processes the command-line arguments which are passed in, and -// then passes execution along to an appropriate binary housed in AppDir/bin -// (usually a bash script, which is more versatile than a go program). - func getAppDirPath() string { appDirPath := os.Getenv("APPDIR") if appDirPath == "" { @@ -26,39 +20,10 @@ func getAppDirPath() string { return appDirPath } -func getRPCSocketDirPath() string { - path, err := firstExistingDir( - "/run", - "/var/run", - "/tmp", - "/dev/shm", - ) - if err != nil { - panic(fmt.Sprintf("Failed to find directory for RPC socket: %v", err)) - } - - return path -} - -// RUNTIME_DIRECTORY/STATE_DIRECTORY are used by the systemd service in -// conjunction with the RuntimeDirectory/StateDirectory directives. var ( - envAppDirPath = getAppDirPath() - envRuntimeDirPath = envOr( - "RUNTIME_DIRECTORY", - func() string { return filepath.Join(xdg.RuntimeDir, "isle") }, - ) - envStateDirPath = envOr( - "STATE_DIRECTORY", - func() string { return filepath.Join(xdg.StateHome, "isle") }, - ) + daemonEnvVars = daemon.GetEnvVars() + envAppDirPath = getAppDirPath() envBinDirPath = filepath.Join(envAppDirPath, "bin") - envSocketPath = envOr( - "ISLE_SOCKET_PATH", - func() string { - return filepath.Join(getRPCSocketDirPath(), "isle-daemon.sock") - }, - ) ) func binPath(name string) string { diff --git a/go/cmd/entrypoint/proc_lock.go b/go/cmd/entrypoint/proc_lock.go index 862bc76..32bfbfd 100644 --- a/go/cmd/entrypoint/proc_lock.go +++ b/go/cmd/entrypoint/proc_lock.go @@ -16,7 +16,7 @@ import ( var errDaemonNotRunning = errors.New("no isle daemon process running") func lockFilePath() string { - return filepath.Join(envRuntimeDirPath, "lock") + return filepath.Join(daemonEnvVars.RuntimeDirPath, "lock") } func writeLock() error { @@ -49,11 +49,11 @@ func writeLock() error { // returns a cleanup function which will clean up the created runtime directory. func setupAndLockRuntimeDir(ctx context.Context, logger *mlog.Logger) (func(), error) { - ctx = mctx.Annotate(ctx, "runtimeDirPath", envRuntimeDirPath) + ctx = mctx.Annotate(ctx, "runtimeDirPath", daemonEnvVars.RuntimeDirPath) logger.Info(ctx, "will use runtime directory for temporary state") - if err := os.MkdirAll(envRuntimeDirPath, 0700); err != nil { - return nil, fmt.Errorf("creating directory %q: %w", envRuntimeDirPath, err) + if err := os.MkdirAll(daemonEnvVars.RuntimeDirPath, 0700); err != nil { + return nil, fmt.Errorf("creating directory %q: %w", daemonEnvVars.RuntimeDirPath, err) } else if err := writeLock(); err != nil { return nil, err @@ -61,7 +61,7 @@ func setupAndLockRuntimeDir(ctx context.Context, logger *mlog.Logger) (func(), e return func() { logger.Info(ctx, "cleaning up runtime directory") - if err := os.RemoveAll(envRuntimeDirPath); err != nil { + if err := os.RemoveAll(daemonEnvVars.RuntimeDirPath); err != nil { logger.Error(ctx, "removing temporary directory", err) } }, nil diff --git a/go/cmd/entrypoint/sub_cmd.go b/go/cmd/entrypoint/sub_cmd.go index cf15281..90b521c 100644 --- a/go/cmd/entrypoint/sub_cmd.go +++ b/go/cmd/entrypoint/sub_cmd.go @@ -110,7 +110,7 @@ func (ctx subCmdCtx) doSubCmd(subCmds ...subCmd) error { } daemonRCPClient := jsonrpc2.NewUnixHTTPClient( - envSocketPath, daemonHTTPRPCPath, + daemonEnvVars.HTTPSocketPath, daemonHTTPRPCPath, ) err := subCmd.do(subCmdCtx{ diff --git a/go/daemon/daemon.go b/go/daemon/daemon.go index 5dbd018..16c0992 100644 --- a/go/daemon/daemon.go +++ b/go/daemon/daemon.go @@ -18,7 +18,7 @@ type daemon struct { logger *mlog.Logger config Config hostBootstrap bootstrap.Bootstrap - stateDirPath string + binDirPath string opts Opts pmuxCancelFn context.CancelFunc @@ -56,6 +56,8 @@ type Opts struct { // Stdout and Stderr are what the associated outputs from child processes // will be directed to. Stdout, Stderr io.Writer + + EnvVars EnvVars // Defaults to that returned by GetEnvVars. } func (o *Opts) withDefaults() *Opts { @@ -71,6 +73,10 @@ func (o *Opts) withDefaults() *Opts { o.Stderr = os.Stderr } + if o.EnvVars == (EnvVars{}) { + o.EnvVars = GetEnvVars() + } + return o } @@ -81,7 +87,7 @@ func New( logger *mlog.Logger, config Config, hostBootstrap bootstrap.Bootstrap, - runtimeDirPath, binDirPath, stateDirPath string, + binDirPath string, opts *Opts, ) ( Daemon, error, @@ -89,21 +95,27 @@ func New( opts = opts.withDefaults() nebulaPmuxProcConfig, err := nebulaPmuxProcConfig( - runtimeDirPath, binDirPath, hostBootstrap, config, + opts.EnvVars.RuntimeDirPath, + binDirPath, + hostBootstrap, + config, ) if err != nil { return nil, fmt.Errorf("generating nebula config: %w", err) } dnsmasqPmuxProcConfig, err := dnsmasqPmuxProcConfig( - runtimeDirPath, binDirPath, hostBootstrap, config, + opts.EnvVars.RuntimeDirPath, + binDirPath, + hostBootstrap, + config, ) if err != nil { return nil, fmt.Errorf("generating dnsmasq config: %w", err) } garagePmuxProcConfigs, err := garagePmuxProcConfigs( - runtimeDirPath, binDirPath, hostBootstrap, config, + opts.EnvVars.RuntimeDirPath, binDirPath, hostBootstrap, config, ) if err != nil { return nil, fmt.Errorf("generating garage children configs: %w", err) @@ -125,7 +137,7 @@ func New( logger: logger, config: config, hostBootstrap: hostBootstrap, - stateDirPath: stateDirPath, + binDirPath: binDirPath, opts: *opts, pmuxCancelFn: pmuxCancelFn, pmuxStoppedCh: make(chan struct{}), diff --git a/go/daemon/env.go b/go/daemon/env.go new file mode 100644 index 0000000..57136c6 --- /dev/null +++ b/go/daemon/env.go @@ -0,0 +1,102 @@ +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 // TODO should be private to this package + StateDirPath string // TODO should be private to this package + HTTPSocketPath string +} + +func getRPCSocketDirPath() string { + path, err := firstExistingDir( + "/run", + "/var/run", + "/tmp", + "/dev/shm", + ) + if err != nil { + panic(fmt.Sprintf("Failed to find directory for RPC socket: %v", err)) + } + + return path +} + +// 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") }, + ) + + v.HTTPSocketPath = envOr( + "ISLE_SOCKET_PATH", + func() string { + return filepath.Join(getRPCSocketDirPath(), "isle-daemon.sock") + }, + ) + + 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 +}