fix(daemon): root local state at DataDir not BaseDir

LocalStatePath was falling back to BaseDir when DataDir was empty, putting
the file at /var/lib/spinifex/spinifex/state/instance-state.json (under the
daemon's own subdir) instead of the documented
/var/lib/spinifex/state/instance-state.json (the platform-shared root).

Drop the BaseDir fallback. Empty DataDir resolves to DefaultDataDir
(/var/lib/spinifex). Tests now set DataDir alongside BaseDir on their config
fixtures.
This commit is contained in:
joshsiv-mulga
2026-04-27 16:07:35 +10:00
parent 7eb388a2c2
commit 8eb9c5fa10
5 changed files with 29 additions and 34 deletions
+2 -2
View File
@@ -1492,9 +1492,9 @@ const kvSyncTimeout = time.Second
// localStatePath returns the on-disk path to this daemon's instance state file. // localStatePath returns the on-disk path to this daemon's instance state file.
func (d *Daemon) localStatePath() string { func (d *Daemon) localStatePath() string {
if d.config == nil { if d.config == nil {
return LocalStatePath("", "") return LocalStatePath("")
} }
return LocalStatePath(d.config.DataDir, d.config.BaseDir) return LocalStatePath(d.config.DataDir)
} }
// WriteState persists the instance state. Local file is the source of truth; // WriteState persists the instance state. Local file is the source of truth;
+12 -9
View File
@@ -52,6 +52,7 @@ func createTestDaemon(t *testing.T, natsURL string) *Daemon {
cfg := &config.Config{ cfg := &config.Config{
BaseDir: tmpDir, BaseDir: tmpDir,
DataDir: tmpDir,
WalDir: tmpDir, WalDir: tmpDir,
NATS: config.NATSConfig{ NATS: config.NATSConfig{
Host: natsURL, Host: natsURL,
@@ -364,6 +365,7 @@ func TestDaemon_Initialization(t *testing.T) {
cfg := &config.Config{ cfg := &config.Config{
BaseDir: tmpDir, BaseDir: tmpDir,
DataDir: tmpDir,
WalDir: tmpDir, WalDir: tmpDir,
NATS: config.NATSConfig{ NATS: config.NATSConfig{
Host: "nats://localhost:4222", Host: "nats://localhost:4222",
@@ -898,11 +900,11 @@ func TestDaemon_BootAllocation(t *testing.T) {
// Create daemon with NATS connection // Create daemon with NATS connection
clusterCfg := &config.ClusterConfig{ clusterCfg := &config.ClusterConfig{
Node: "node-1", Node: "node-1",
Nodes: map[string]config.Config{"node-1": {BaseDir: tmpDir}}, Nodes: map[string]config.Config{"node-1": {BaseDir: tmpDir, DataDir: tmpDir}},
} }
daemon, err := NewDaemon(clusterCfg) daemon, err := NewDaemon(clusterCfg)
require.NoError(t, err) require.NoError(t, err)
daemon.config = &config.Config{BaseDir: tmpDir} daemon.config = &config.Config{BaseDir: tmpDir, DataDir: tmpDir}
// Connect to NATS and initialize JetStream // Connect to NATS and initialize JetStream
nc, err := nats.Connect(natsURL) nc, err := nats.Connect(natsURL)
@@ -3133,12 +3135,12 @@ func TestDaemon_WriteState_NilJSManager(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
d := &Daemon{ d := &Daemon{
jsManager: nil, jsManager: nil,
config: &config.Config{BaseDir: tmpDir}, config: &config.Config{DataDir: tmpDir},
Instances: vm.Instances{VMS: map[string]*vm.VM{"i-1": {ID: "i-1"}}}, Instances: vm.Instances{VMS: map[string]*vm.VM{"i-1": {ID: "i-1"}}},
} }
require.NoError(t, d.WriteState()) require.NoError(t, d.WriteState())
state, err := ReadLocalState(LocalStatePath("", tmpDir)) state, err := ReadLocalState(LocalStatePath(tmpDir))
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, state) require.NotNil(t, state)
assert.Contains(t, state.VMS, "i-1") assert.Contains(t, state.VMS, "i-1")
@@ -3146,13 +3148,13 @@ func TestDaemon_WriteState_NilJSManager(t *testing.T) {
func TestDaemon_LoadState_NilJSManager(t *testing.T) { func TestDaemon_LoadState_NilJSManager(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
require.NoError(t, WriteLocalState(LocalStatePath("", tmpDir), &vm.Instances{ require.NoError(t, WriteLocalState(LocalStatePath(tmpDir), &vm.Instances{
VMS: map[string]*vm.VM{"i-seed": {ID: "i-seed"}}, VMS: map[string]*vm.VM{"i-seed": {ID: "i-seed"}},
})) }))
d := &Daemon{ d := &Daemon{
jsManager: nil, jsManager: nil,
config: &config.Config{BaseDir: tmpDir}, config: &config.Config{DataDir: tmpDir},
} }
require.NoError(t, d.LoadState()) require.NoError(t, d.LoadState())
assert.Contains(t, d.Instances.VMS, "i-seed") assert.Contains(t, d.Instances.VMS, "i-seed")
@@ -3161,7 +3163,7 @@ func TestDaemon_LoadState_NilJSManager(t *testing.T) {
func TestDaemon_LoadState_MissingFileIsFreshInstall(t *testing.T) { func TestDaemon_LoadState_MissingFileIsFreshInstall(t *testing.T) {
d := &Daemon{ d := &Daemon{
jsManager: nil, jsManager: nil,
config: &config.Config{BaseDir: t.TempDir()}, config: &config.Config{DataDir: t.TempDir()},
} }
require.NoError(t, d.LoadState()) require.NoError(t, d.LoadState())
assert.Empty(t, d.Instances.VMS) assert.Empty(t, d.Instances.VMS)
@@ -3169,13 +3171,13 @@ func TestDaemon_LoadState_MissingFileIsFreshInstall(t *testing.T) {
func TestDaemon_LoadState_CorruptFileFatal(t *testing.T) { func TestDaemon_LoadState_CorruptFileFatal(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
path := LocalStatePath("", tmpDir) path := LocalStatePath(tmpDir)
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o750)) require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o750))
require.NoError(t, os.WriteFile(path, []byte("{not json"), 0o600)) require.NoError(t, os.WriteFile(path, []byte("{not json"), 0o600))
d := &Daemon{ d := &Daemon{
jsManager: nil, jsManager: nil,
config: &config.Config{BaseDir: tmpDir}, config: &config.Config{DataDir: tmpDir},
} }
err := d.LoadState() err := d.LoadState()
require.Error(t, err) require.Error(t, err)
@@ -4061,6 +4063,7 @@ func TestClusterManager_TLSServesHTTPS(t *testing.T) {
Nodes: map[string]config.Config{ Nodes: map[string]config.Config{
"node-1": { "node-1": {
BaseDir: tmpDir, BaseDir: tmpDir,
DataDir: tmpDir,
Daemon: config.DaemonConfig{ Daemon: config.DaemonConfig{
Host: addr, Host: addr,
TLSCert: certPath, TLSCert: certPath,
+9 -12
View File
@@ -34,19 +34,16 @@ type LocalState struct {
VMS map[string]*vm.VM `json:"vms"` VMS map[string]*vm.VM `json:"vms"`
} }
// LocalStatePath returns the absolute path to the per-node instance state file. // LocalStatePath returns the absolute path to the per-node instance state file
// Resolution order: dataDir, then baseDir, then DefaultDataDir. The first // rooted at dataDir. Empty dataDir falls back to DefaultDataDir
// non-empty value wins. Production sets dataDir (/var/lib/spinifex); tests // (/var/lib/spinifex), the platform-shared root. The state directory sits at
// typically set baseDir to a t.TempDir(). // the platform root so it lives next to other shared state (predastore,
func LocalStatePath(dataDir, baseDir string) string { // viperblock data dirs) rather than nested under the daemon's own subdir.
root := dataDir func LocalStatePath(dataDir string) string {
if root == "" { if dataDir == "" {
root = baseDir dataDir = DefaultDataDir
} }
if root == "" { return filepath.Join(dataDir, DefaultLocalStateDir, LocalStateFileName)
root = DefaultDataDir
}
return filepath.Join(root, DefaultLocalStateDir, LocalStateFileName)
} }
// WriteLocalState atomically writes the instance state to path. // WriteLocalState atomically writes the instance state to path.
+4 -9
View File
@@ -11,18 +11,13 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestLocalStatePath_PrefersDataDir(t *testing.T) { func TestLocalStatePath_UsesDataDir(t *testing.T) {
got := LocalStatePath("/data", "/base") got := LocalStatePath("/data")
assert.Equal(t, "/data/state/instance-state.json", got) assert.Equal(t, "/data/state/instance-state.json", got)
} }
func TestLocalStatePath_FallsBackToBaseDir(t *testing.T) { func TestLocalStatePath_DefaultWhenEmpty(t *testing.T) {
got := LocalStatePath("", "/base") got := LocalStatePath("")
assert.Equal(t, "/base/state/instance-state.json", got)
}
func TestLocalStatePath_DefaultWhenBothEmpty(t *testing.T) {
got := LocalStatePath("", "")
assert.Equal(t, "/var/lib/spinifex/state/instance-state.json", got) assert.Equal(t, "/var/lib/spinifex/state/instance-state.json", got)
} }
+2 -2
View File
@@ -28,11 +28,11 @@ func createDaemonWithJetStream(t *testing.T) *Daemon {
clusterCfg := &config.ClusterConfig{ clusterCfg := &config.ClusterConfig{
Node: "node-1", Node: "node-1",
Nodes: map[string]config.Config{"node-1": {BaseDir: tmpDir}}, Nodes: map[string]config.Config{"node-1": {BaseDir: tmpDir, DataDir: tmpDir}},
} }
daemon, err := NewDaemon(clusterCfg) daemon, err := NewDaemon(clusterCfg)
require.NoError(t, err) require.NoError(t, err)
daemon.config = &config.Config{BaseDir: tmpDir} daemon.config = &config.Config{BaseDir: tmpDir, DataDir: tmpDir}
daemon.natsConn = nc daemon.natsConn = nc
daemon.jsManager, err = NewJetStreamManager(nc, 1) daemon.jsManager, err = NewJetStreamManager(nc, 1)