diff --git a/pkg/instances/manager.go b/pkg/instances/manager.go index c13a5a6..61ecf66 100644 --- a/pkg/instances/manager.go +++ b/pkg/instances/manager.go @@ -30,6 +30,8 @@ import ( const ( // DefaultStorageFileName is the default filename for storing instances DefaultStorageFileName = "instances.json" + // RuntimesSubdirectory is the subdirectory for runtime storage + RuntimesSubdirectory = "runtimes" ) // InstanceFactory is a function that creates an Instance from InstanceData @@ -70,7 +72,12 @@ var _ Manager = (*manager)(nil) // NewManager creates a new instance manager with the given storage directory. func NewManager(storageDir string) (Manager, error) { - return newManagerWithFactory(storageDir, NewInstanceFromData, generator.New(), runtime.NewRegistry()) + runtimesDir := filepath.Join(storageDir, RuntimesSubdirectory) + reg, err := runtime.NewRegistry(runtimesDir) + if err != nil { + return nil, fmt.Errorf("failed to create runtime registry: %w", err) + } + return newManagerWithFactory(storageDir, NewInstanceFromData, generator.New(), reg) } // newManagerWithFactory creates a new instance manager with a custom instance factory, generator, and registry. diff --git a/pkg/instances/manager_test.go b/pkg/instances/manager_test.go index 341cb88..5c427ed 100644 --- a/pkg/instances/manager_test.go +++ b/pkg/instances/manager_test.go @@ -171,8 +171,12 @@ func (g *fakeSequentialGenerator) Generate() string { } // newTestRegistry creates a runtime registry with a fake runtime for testing -func newTestRegistry() runtime.Registry { - reg := runtime.NewRegistry() +func newTestRegistry(storageDir string) runtime.Registry { + runtimesDir := filepath.Join(storageDir, RuntimesSubdirectory) + reg, err := runtime.NewRegistry(runtimesDir) + if err != nil { + panic(fmt.Sprintf("failed to create test registry: %v", err)) + } _ = reg.Register(fake.New()) return reg } @@ -235,7 +239,7 @@ func TestNewManager(t *testing.T) { t.Parallel() tmpDir := t.TempDir() - manager, err := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry()) + manager, err := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry(tmpDir)) if err != nil { t.Fatalf("newManagerWithFactory() unexpected error = %v", err) } @@ -267,7 +271,7 @@ func TestManager_Add(t *testing.T) { t.Parallel() tmpDir := t.TempDir() - manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry()) + manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry(tmpDir)) instanceTmpDir := t.TempDir() inst := newFakeInstance(newFakeInstanceParams{ @@ -297,7 +301,7 @@ func TestManager_Add(t *testing.T) { t.Parallel() tmpDir := t.TempDir() - manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry()) + manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry(tmpDir)) _, err := manager.Add(context.Background(), nil, "fake") if err == nil { @@ -319,7 +323,7 @@ func TestManager_Add(t *testing.T) { "duplicate-id-0000000000000000000000000000000000000000000000000000000a", "unique-id-1-0000000000000000000000000000000000000000000000000000000b", }) - manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, gen, newTestRegistry()) + manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, gen, newTestRegistry(tmpDir)) instanceTmpDir := t.TempDir() // Create instances without IDs (empty ID) @@ -370,7 +374,7 @@ func TestManager_Add(t *testing.T) { t.Parallel() tmpDir := t.TempDir() - manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry()) + manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry(tmpDir)) instanceTmpDir := t.TempDir() inst := newFakeInstance(newFakeInstanceParams{ @@ -396,7 +400,7 @@ func TestManager_Add(t *testing.T) { ctx := context.Background() tmpDir := t.TempDir() - manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry()) + manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry(tmpDir)) instanceTmpDir := t.TempDir() inst1 := newFakeInstance(newFakeInstanceParams{ @@ -433,7 +437,7 @@ func TestManager_List(t *testing.T) { t.Parallel() tmpDir := t.TempDir() - manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry()) + manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry(tmpDir)) instances, err := manager.List() if err != nil { @@ -449,7 +453,7 @@ func TestManager_List(t *testing.T) { ctx := context.Background() tmpDir := t.TempDir() - manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry()) + manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry(tmpDir)) instanceTmpDir := t.TempDir() inst1 := newFakeInstance(newFakeInstanceParams{ @@ -479,7 +483,7 @@ func TestManager_List(t *testing.T) { t.Parallel() tmpDir := t.TempDir() - manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry()) + manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry(tmpDir)) // Create empty storage file storageFile := filepath.Join(tmpDir, DefaultStorageFileName) @@ -502,7 +506,7 @@ func TestManager_Get(t *testing.T) { t.Parallel() tmpDir := t.TempDir() - manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry()) + manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry(tmpDir)) instanceTmpDir := t.TempDir() expectedSource := filepath.Join(instanceTmpDir, "source") @@ -535,7 +539,7 @@ func TestManager_Get(t *testing.T) { t.Parallel() tmpDir := t.TempDir() - manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry()) + manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry(tmpDir)) _, err := manager.Get("nonexistent-id") if err != ErrInstanceNotFound { @@ -552,7 +556,7 @@ func TestManager_Delete(t *testing.T) { ctx := context.Background() tmpDir := t.TempDir() - manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry()) + manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry(tmpDir)) instanceTmpDir := t.TempDir() sourceDir := filepath.Join(instanceTmpDir, "source") @@ -582,7 +586,7 @@ func TestManager_Delete(t *testing.T) { t.Parallel() tmpDir := t.TempDir() - manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry()) + manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry(tmpDir)) err := manager.Delete(context.Background(), "nonexistent-id") if err != ErrInstanceNotFound { @@ -595,7 +599,7 @@ func TestManager_Delete(t *testing.T) { ctx := context.Background() tmpDir := t.TempDir() - manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry()) + manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry(tmpDir)) instanceTmpDir := t.TempDir() source1 := filepath.Join(instanceTmpDir, "source1") @@ -638,7 +642,7 @@ func TestManager_Delete(t *testing.T) { ctx := context.Background() tmpDir := t.TempDir() - manager1, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry()) + manager1, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry(tmpDir)) inst := newFakeInstance(newFakeInstanceParams{ SourceDir: filepath.Join(string(filepath.Separator), "tmp", "source"), @@ -652,7 +656,7 @@ func TestManager_Delete(t *testing.T) { manager1.Delete(ctx, generatedID) // Create new manager with same storage - manager2, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry()) + manager2, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry(tmpDir)) _, err := manager2.Get(generatedID) if err != ErrInstanceNotFound { t.Errorf("Get() from new manager error = %v, want %v", err, ErrInstanceNotFound) @@ -668,7 +672,7 @@ func TestManager_Start(t *testing.T) { ctx := context.Background() tmpDir := t.TempDir() - manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry()) + manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry(tmpDir)) instanceTmpDir := t.TempDir() inst := newFakeInstance(newFakeInstanceParams{ @@ -700,7 +704,7 @@ func TestManager_Start(t *testing.T) { t.Parallel() tmpDir := t.TempDir() - manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry()) + manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry(tmpDir)) err := manager.Start(context.Background(), "nonexistent-id") if err != ErrInstanceNotFound { @@ -730,7 +734,7 @@ func TestManager_Start(t *testing.T) { runtime: RuntimeData{}, // Empty runtime }, nil } - manager, _ := newManagerWithFactory(tmpDir, noRuntimeFactory, newFakeGenerator(), newTestRegistry()) + manager, _ := newManagerWithFactory(tmpDir, noRuntimeFactory, newFakeGenerator(), newTestRegistry(tmpDir)) inst := newFakeInstance(newFakeInstanceParams{ SourceDir: filepath.Join(string(filepath.Separator), "tmp", "source"), @@ -770,7 +774,7 @@ func TestManager_Start(t *testing.T) { ctx := context.Background() tmpDir := t.TempDir() - manager1, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry()) + manager1, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry(tmpDir)) inst := newFakeInstance(newFakeInstanceParams{ SourceDir: filepath.Join(string(filepath.Separator), "tmp", "source"), @@ -781,7 +785,7 @@ func TestManager_Start(t *testing.T) { manager1.Start(ctx, added.GetID()) // Create new manager with same storage - manager2, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry()) + manager2, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry(tmpDir)) retrieved, _ := manager2.Get(added.GetID()) if retrieved.GetRuntimeData().State != "running" { @@ -798,7 +802,7 @@ func TestManager_Stop(t *testing.T) { ctx := context.Background() tmpDir := t.TempDir() - manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry()) + manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry(tmpDir)) instanceTmpDir := t.TempDir() inst := newFakeInstance(newFakeInstanceParams{ @@ -832,7 +836,7 @@ func TestManager_Stop(t *testing.T) { t.Parallel() tmpDir := t.TempDir() - manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry()) + manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry(tmpDir)) err := manager.Stop(context.Background(), "nonexistent-id") if err != ErrInstanceNotFound { @@ -845,7 +849,7 @@ func TestManager_Stop(t *testing.T) { ctx := context.Background() tmpDir := t.TempDir() - manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry()) + manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry(tmpDir)) inst := newFakeInstance(newFakeInstanceParams{ SourceDir: filepath.Join(string(filepath.Separator), "tmp", "source"), @@ -885,7 +889,7 @@ func TestManager_Stop(t *testing.T) { ctx := context.Background() tmpDir := t.TempDir() - manager1, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry()) + manager1, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry(tmpDir)) inst := newFakeInstance(newFakeInstanceParams{ SourceDir: filepath.Join(string(filepath.Separator), "tmp", "source"), @@ -897,7 +901,7 @@ func TestManager_Stop(t *testing.T) { manager1.Stop(ctx, added.GetID()) // Create new manager with same storage - manager2, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry()) + manager2, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry(tmpDir)) retrieved, _ := manager2.Get(added.GetID()) if retrieved.GetRuntimeData().State != "stopped" { @@ -910,7 +914,7 @@ func TestManager_Stop(t *testing.T) { ctx := context.Background() tmpDir := t.TempDir() - manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry()) + manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry(tmpDir)) instanceTmpDir := t.TempDir() inst := newFakeInstance(newFakeInstanceParams{ @@ -954,7 +958,7 @@ func TestManager_Reconcile(t *testing.T) { accessible: false, // Always inaccessible for this test }, nil } - manager, _ := newManagerWithFactory(tmpDir, inaccessibleFactory, newFakeGenerator(), newTestRegistry()) + manager, _ := newManagerWithFactory(tmpDir, inaccessibleFactory, newFakeGenerator(), newTestRegistry(tmpDir)) // Add instance that is inaccessible instanceTmpDir := t.TempDir() @@ -998,7 +1002,7 @@ func TestManager_Reconcile(t *testing.T) { accessible: false, // Always inaccessible for this test }, nil } - manager, _ := newManagerWithFactory(tmpDir, inaccessibleFactory, newFakeGenerator(), newTestRegistry()) + manager, _ := newManagerWithFactory(tmpDir, inaccessibleFactory, newFakeGenerator(), newTestRegistry(tmpDir)) // Add instance that is inaccessible instanceTmpDir := t.TempDir() @@ -1042,7 +1046,7 @@ func TestManager_Reconcile(t *testing.T) { accessible: false, // Always inaccessible for this test }, nil } - manager, _ := newManagerWithFactory(tmpDir, inaccessibleFactory, newFakeGenerator(), newTestRegistry()) + manager, _ := newManagerWithFactory(tmpDir, inaccessibleFactory, newFakeGenerator(), newTestRegistry(tmpDir)) instanceTmpDir := t.TempDir() inaccessibleSource := filepath.Join(instanceTmpDir, "nonexistent-source") @@ -1091,7 +1095,7 @@ func TestManager_Reconcile(t *testing.T) { accessible: accessible, }, nil } - manager, _ := newManagerWithFactory(tmpDir, mixedFactory, newFakeGenerator(), newTestRegistry()) + manager, _ := newManagerWithFactory(tmpDir, mixedFactory, newFakeGenerator(), newTestRegistry(tmpDir)) accessibleConfig := filepath.Join(instanceTmpDir, "accessible-config") @@ -1134,7 +1138,7 @@ func TestManager_Reconcile(t *testing.T) { t.Parallel() tmpDir := t.TempDir() - manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry()) + manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry(tmpDir)) instanceTmpDir := t.TempDir() inst := newFakeInstance(newFakeInstanceParams{ @@ -1158,7 +1162,7 @@ func TestManager_Reconcile(t *testing.T) { t.Parallel() tmpDir := t.TempDir() - manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry()) + manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry(tmpDir)) removed, err := manager.Reconcile() if err != nil { @@ -1181,7 +1185,7 @@ func TestManager_Persistence(t *testing.T) { instanceTmpDir := t.TempDir() // Create first manager and add instance - manager1, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry()) + manager1, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry(tmpDir)) expectedSource := filepath.Join(instanceTmpDir, "source") expectedConfig := filepath.Join(instanceTmpDir, "config") inst := newFakeInstance(newFakeInstanceParams{ @@ -1194,7 +1198,7 @@ func TestManager_Persistence(t *testing.T) { generatedID := added.GetID() // Create second manager with same storage - manager2, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry()) + manager2, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry(tmpDir)) instances, err := manager2.List() if err != nil { t.Fatalf("List() from second manager unexpected error = %v", err) @@ -1215,7 +1219,7 @@ func TestManager_Persistence(t *testing.T) { t.Parallel() tmpDir := t.TempDir() - manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry()) + manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry(tmpDir)) instanceTmpDir := t.TempDir() expectedSource := filepath.Join(instanceTmpDir, "source") @@ -1267,7 +1271,7 @@ func TestManager_ConcurrentAccess(t *testing.T) { t.Parallel() tmpDir := t.TempDir() - manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry()) + manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry(tmpDir)) instanceTmpDir := t.TempDir() var wg sync.WaitGroup @@ -1300,7 +1304,7 @@ func TestManager_ConcurrentAccess(t *testing.T) { t.Parallel() tmpDir := t.TempDir() - manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry()) + manager, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry(tmpDir)) instanceTmpDir := t.TempDir() // Add some instances first @@ -1344,7 +1348,7 @@ func TestManager_ensureUniqueName(t *testing.T) { t.Parallel() tmpDir := t.TempDir() - m, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry()) + m, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry(tmpDir)) // Cast to concrete type to access unexported methods mgr := m.(*manager) @@ -1377,7 +1381,7 @@ func TestManager_ensureUniqueName(t *testing.T) { t.Parallel() tmpDir := t.TempDir() - m, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry()) + m, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry(tmpDir)) mgr := m.(*manager) @@ -1402,7 +1406,7 @@ func TestManager_ensureUniqueName(t *testing.T) { t.Parallel() tmpDir := t.TempDir() - m, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry()) + m, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry(tmpDir)) mgr := m.(*manager) @@ -1441,7 +1445,7 @@ func TestManager_ensureUniqueName(t *testing.T) { t.Parallel() tmpDir := t.TempDir() - m, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry()) + m, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry(tmpDir)) mgr := m.(*manager) @@ -1477,7 +1481,7 @@ func TestManager_ensureUniqueName(t *testing.T) { t.Parallel() tmpDir := t.TempDir() - m, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry()) + m, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry(tmpDir)) mgr := m.(*manager) @@ -1498,7 +1502,7 @@ func TestManager_generateUniqueName(t *testing.T) { t.Parallel() tmpDir := t.TempDir() - m, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry()) + m, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry(tmpDir)) mgr := m.(*manager) @@ -1515,7 +1519,7 @@ func TestManager_generateUniqueName(t *testing.T) { t.Parallel() tmpDir := t.TempDir() - m, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry()) + m, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry(tmpDir)) mgr := m.(*manager) @@ -1540,7 +1544,7 @@ func TestManager_generateUniqueName(t *testing.T) { t.Parallel() tmpDir := t.TempDir() - m, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry()) + m, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry(tmpDir)) mgr := m.(*manager) @@ -1558,7 +1562,7 @@ func TestManager_generateUniqueName(t *testing.T) { t.Parallel() tmpDir := t.TempDir() - m, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry()) + m, _ := newManagerWithFactory(tmpDir, fakeInstanceFactory, newFakeGenerator(), newTestRegistry(tmpDir)) mgr := m.(*manager) diff --git a/pkg/runtime/registry.go b/pkg/runtime/registry.go index 9f52dc5..a3ccf12 100644 --- a/pkg/runtime/registry.go +++ b/pkg/runtime/registry.go @@ -16,6 +16,8 @@ package runtime import ( "fmt" + "os" + "path/filepath" "sync" ) @@ -33,20 +35,73 @@ type Registry interface { List() []string } +// StorageAware is an optional interface that runtimes can implement +// to receive a dedicated storage directory during registration. +// +// When a runtime implements this interface, the Registry will: +// 1. Create a directory at REGISTRY_STORAGE/runtimeType +// 2. Call Initialize with the absolute path to this directory +// 3. The runtime can use this directory to persist private data +// +// Example implementation: +// +// type myRuntime struct { +// storageDir string +// } +// +// func (r *myRuntime) Initialize(storageDir string) error { +// r.storageDir = storageDir +// // Optional: create subdirectories, load state, etc. +// return os.MkdirAll(filepath.Join(storageDir, "instances"), 0755) +// } +// +// func (r *myRuntime) Create(ctx context.Context, params CreateParams) (RuntimeInfo, error) { +// // Use r.storageDir to persist instance data +// instanceFile := filepath.Join(r.storageDir, "instances", id+".json") +// return os.WriteFile(instanceFile, data, 0644) +// } +type StorageAware interface { + // Initialize is called during registration with the runtime's private storage directory. + // The directory is created before this method is called and is guaranteed to exist. + // The path will be: REGISTRY_STORAGE/runtimeType + // + // Runtimes should store the directory path and use it for persisting private data. + // This method is called exactly once during registration, before the runtime is available for use. + Initialize(storageDir string) error +} + // registry is the concrete implementation of Registry. type registry struct { - mu sync.RWMutex - runtimes map[string]Runtime + mu sync.RWMutex + runtimes map[string]Runtime + storageDir string } // Ensure registry implements Registry interface at compile time. var _ Registry = (*registry)(nil) -// NewRegistry creates a new empty runtime registry. -func NewRegistry() Registry { - return ®istry{ - runtimes: make(map[string]Runtime), +// NewRegistry creates a new runtime registry with the specified storage directory. +// The storage directory is used to create runtime-specific subdirectories at REGISTRY_STORAGE/runtimeType. +func NewRegistry(storageDir string) (Registry, error) { + if storageDir == "" { + return nil, fmt.Errorf("storage directory cannot be empty") } + + // Convert to absolute path + absStorageDir, err := filepath.Abs(storageDir) + if err != nil { + return nil, fmt.Errorf("failed to resolve storage directory: %w", err) + } + + // Create storage directory if it doesn't exist + if err := os.MkdirAll(absStorageDir, 0755); err != nil { + return nil, fmt.Errorf("failed to create storage directory: %w", err) + } + + return ®istry{ + runtimes: make(map[string]Runtime), + storageDir: absStorageDir, + }, nil } // Register adds a runtime to the registry. @@ -67,6 +122,19 @@ func (r *registry) Register(runtime Runtime) error { return fmt.Errorf("runtime already registered: %s", runtimeType) } + // Initialize runtime with storage directory if it supports it + if storageAware, ok := runtime.(StorageAware); ok { + // Create runtime-specific storage directory + runtimeStorageDir := filepath.Join(r.storageDir, runtimeType) + if err := os.MkdirAll(runtimeStorageDir, 0755); err != nil { + return fmt.Errorf("failed to create runtime storage directory: %w", err) + } + + if err := storageAware.Initialize(runtimeStorageDir); err != nil { + return fmt.Errorf("failed to initialize runtime storage: %w", err) + } + } + r.runtimes[runtimeType] = runtime return nil } diff --git a/pkg/runtime/registry_test.go b/pkg/runtime/registry_test.go index 1b72674..f5ddda2 100644 --- a/pkg/runtime/registry_test.go +++ b/pkg/runtime/registry_test.go @@ -18,6 +18,8 @@ import ( "context" "errors" "fmt" + "os" + "path/filepath" "testing" ) @@ -53,11 +55,16 @@ func (f *fakeRuntime) Info(ctx context.Context, id string) (RuntimeInfo, error) func TestRegistry_RegisterAndGet(t *testing.T) { t.Parallel() - reg := NewRegistry() + storageDir := t.TempDir() + reg, err := NewRegistry(storageDir) + if err != nil { + t.Fatalf("Failed to create registry: %v", err) + } + rt := &fakeRuntime{typeID: "test-runtime"} // Register the runtime - err := reg.Register(rt) + err = reg.Register(rt) if err != nil { t.Fatalf("Failed to register runtime: %v", err) } @@ -76,12 +83,17 @@ func TestRegistry_RegisterAndGet(t *testing.T) { func TestRegistry_DuplicateRegistration(t *testing.T) { t.Parallel() - reg := NewRegistry() + storageDir := t.TempDir() + reg, err := NewRegistry(storageDir) + if err != nil { + t.Fatalf("Failed to create registry: %v", err) + } + rt1 := &fakeRuntime{typeID: "test-runtime"} rt2 := &fakeRuntime{typeID: "test-runtime"} // Register first runtime - err := reg.Register(rt1) + err = reg.Register(rt1) if err != nil { t.Fatalf("Failed to register first runtime: %v", err) } @@ -101,10 +113,14 @@ func TestRegistry_DuplicateRegistration(t *testing.T) { func TestRegistry_GetUnknownRuntime(t *testing.T) { t.Parallel() - reg := NewRegistry() + storageDir := t.TempDir() + reg, err := NewRegistry(storageDir) + if err != nil { + t.Fatalf("Failed to create registry: %v", err) + } // Try to get non-existent runtime - _, err := reg.Get("unknown-runtime") + _, err = reg.Get("unknown-runtime") if err == nil { t.Fatal("Expected error when getting unknown runtime, got nil") } @@ -117,7 +133,11 @@ func TestRegistry_GetUnknownRuntime(t *testing.T) { func TestRegistry_List(t *testing.T) { t.Parallel() - reg := NewRegistry() + storageDir := t.TempDir() + reg, err := NewRegistry(storageDir) + if err != nil { + t.Fatalf("Failed to create registry: %v", err) + } // Empty registry types := reg.List() @@ -156,9 +176,13 @@ func TestRegistry_List(t *testing.T) { func TestRegistry_RegisterNil(t *testing.T) { t.Parallel() - reg := NewRegistry() + storageDir := t.TempDir() + reg, err := NewRegistry(storageDir) + if err != nil { + t.Fatalf("Failed to create registry: %v", err) + } - err := reg.Register(nil) + err = reg.Register(nil) if err == nil { t.Fatal("Expected error when registering nil runtime, got nil") } @@ -172,10 +196,15 @@ func TestRegistry_RegisterNil(t *testing.T) { func TestRegistry_RegisterEmptyType(t *testing.T) { t.Parallel() - reg := NewRegistry() + storageDir := t.TempDir() + reg, err := NewRegistry(storageDir) + if err != nil { + t.Fatalf("Failed to create registry: %v", err) + } + rt := &fakeRuntime{typeID: ""} - err := reg.Register(rt) + err = reg.Register(rt) if err == nil { t.Fatal("Expected error when registering runtime with empty type, got nil") } @@ -189,7 +218,11 @@ func TestRegistry_RegisterEmptyType(t *testing.T) { func TestRegistry_ThreadSafety(t *testing.T) { t.Parallel() - reg := NewRegistry() + storageDir := t.TempDir() + reg, err := NewRegistry(storageDir) + if err != nil { + t.Fatalf("Failed to create registry: %v", err) + } // Spawn multiple goroutines that register, get, and list concurrently const numGoroutines = 10 @@ -234,3 +267,222 @@ func TestRegistry_ThreadSafety(t *testing.T) { t.Errorf("Expected %d registered runtimes, got %d", numGoroutines, len(types)) } } + +func TestNewRegistry_EmptyStorageDir(t *testing.T) { + t.Parallel() + + _, err := NewRegistry("") + if err == nil { + t.Fatal("Expected error when creating registry with empty storage directory, got nil") + } + + expectedMsg := "storage directory cannot be empty" + if err.Error() != expectedMsg { + t.Errorf("Expected error message '%s', got '%s'", expectedMsg, err.Error()) + } +} + +func TestNewRegistry_CreatesStorageDir(t *testing.T) { + t.Parallel() + + storageDir := t.TempDir() + nestedDir := filepath.Join(storageDir, "nested", "path") + + reg, err := NewRegistry(nestedDir) + if err != nil { + t.Fatalf("Failed to create registry: %v", err) + } + + if reg == nil { + t.Fatal("Expected registry to be created, got nil") + } + + // Verify directory was created + if _, err := os.Stat(nestedDir); os.IsNotExist(err) { + t.Errorf("Expected storage directory to be created at %s", nestedDir) + } +} + +func TestRegistry_CreatesRuntimeStorageDir(t *testing.T) { + t.Parallel() + + storageDir := t.TempDir() + reg, err := NewRegistry(storageDir) + if err != nil { + t.Fatalf("Failed to create registry: %v", err) + } + + // Only StorageAware runtimes should have directories created + rt := &storageAwareRuntime{typeID: "test-runtime"} + err = reg.Register(rt) + if err != nil { + t.Fatalf("Failed to register runtime: %v", err) + } + + // Verify runtime storage directory was created + expectedPath := filepath.Join(storageDir, "test-runtime") + if _, err := os.Stat(expectedPath); os.IsNotExist(err) { + t.Errorf("Expected runtime storage directory to be created at %s", expectedPath) + } + + // Verify non-StorageAware runtimes do NOT have directories created + rt2 := &fakeRuntime{typeID: "non-storage-runtime"} + err = reg.Register(rt2) + if err != nil { + t.Fatalf("Failed to register non-storage runtime: %v", err) + } + + // Directory should NOT exist for non-StorageAware runtime + nonStoragePath := filepath.Join(storageDir, "non-storage-runtime") + if _, err := os.Stat(nonStoragePath); !os.IsNotExist(err) { + t.Errorf("Expected no directory for non-StorageAware runtime, but found: %s", nonStoragePath) + } +} + +// storageAwareRuntime is a fake runtime that implements StorageAware. +type storageAwareRuntime struct { + typeID string + storageDir string + initializeErr error +} + +func (s *storageAwareRuntime) Type() string { + return s.typeID +} + +func (s *storageAwareRuntime) Create(ctx context.Context, params CreateParams) (RuntimeInfo, error) { + return RuntimeInfo{}, nil +} + +func (s *storageAwareRuntime) Start(ctx context.Context, id string) (RuntimeInfo, error) { + return RuntimeInfo{}, nil +} + +func (s *storageAwareRuntime) Stop(ctx context.Context, id string) error { + return nil +} + +func (s *storageAwareRuntime) Remove(ctx context.Context, id string) error { + return nil +} + +func (s *storageAwareRuntime) Info(ctx context.Context, id string) (RuntimeInfo, error) { + return RuntimeInfo{}, nil +} + +func (s *storageAwareRuntime) Initialize(storageDir string) error { + s.storageDir = storageDir + return s.initializeErr +} + +func TestRegistry_CallsInitialize(t *testing.T) { + t.Parallel() + + storageDir := t.TempDir() + reg, err := NewRegistry(storageDir) + if err != nil { + t.Fatalf("Failed to create registry: %v", err) + } + + rt := &storageAwareRuntime{typeID: "storage-aware"} + err = reg.Register(rt) + if err != nil { + t.Fatalf("Failed to register runtime: %v", err) + } + + // Verify Initialize was called with the correct directory + expectedPath := filepath.Join(storageDir, "storage-aware") + if rt.storageDir != expectedPath { + t.Errorf("Expected Initialize to be called with %s, got %s", expectedPath, rt.storageDir) + } +} + +func TestRegistry_InitializeError(t *testing.T) { + t.Parallel() + + storageDir := t.TempDir() + reg, err := NewRegistry(storageDir) + if err != nil { + t.Fatalf("Failed to create registry: %v", err) + } + + expectedErr := fmt.Errorf("initialization failed") + rt := &storageAwareRuntime{ + typeID: "storage-aware", + initializeErr: expectedErr, + } + + err = reg.Register(rt) + if err == nil { + t.Fatal("Expected error when Initialize fails, got nil") + } + + if !errors.Is(err, expectedErr) { + t.Errorf("Expected error to wrap %v, got %v", expectedErr, err) + } +} + +func TestRegistry_StorageDirectoryStructure(t *testing.T) { + t.Parallel() + + storageDir := t.TempDir() + + // Create registry + reg, err := NewRegistry(storageDir) + if err != nil { + t.Fatalf("Failed to create registry: %v", err) + } + + // Register multiple runtimes, both StorageAware and non-StorageAware + aware1 := &storageAwareRuntime{typeID: "aware1"} + nonAware1 := &fakeRuntime{typeID: "nonAware1"} + aware2 := &storageAwareRuntime{typeID: "aware2"} + + if err := reg.Register(aware1); err != nil { + t.Fatalf("Failed to register aware1 runtime: %v", err) + } + if err := reg.Register(nonAware1); err != nil { + t.Fatalf("Failed to register nonAware1 runtime: %v", err) + } + if err := reg.Register(aware2); err != nil { + t.Fatalf("Failed to register aware2 runtime: %v", err) + } + + // Verify directory structure - only StorageAware runtimes should have directories + expectedDirs := []string{ + filepath.Join(storageDir, "aware1"), + filepath.Join(storageDir, "aware2"), + } + + for _, dir := range expectedDirs { + if _, err := os.Stat(dir); os.IsNotExist(err) { + t.Errorf("Expected directory to exist: %s", dir) + } + } + + // Verify non-StorageAware runtime does NOT have a directory + nonAwareDir := filepath.Join(storageDir, "nonAware1") + if _, err := os.Stat(nonAwareDir); !os.IsNotExist(err) { + t.Errorf("Expected no directory for non-StorageAware runtime (nonAware1), but found: %s", nonAwareDir) + } + + // Verify StorageAware runtimes received their directories + expectedAware1Dir := filepath.Join(storageDir, "aware1") + if aware1.storageDir != expectedAware1Dir { + t.Errorf("Expected aware1 runtime to receive %s, got %s", expectedAware1Dir, aware1.storageDir) + } + + expectedAware2Dir := filepath.Join(storageDir, "aware2") + if aware2.storageDir != expectedAware2Dir { + t.Errorf("Expected aware2 runtime to receive %s, got %s", expectedAware2Dir, aware2.storageDir) + } + + // Verify non-StorageAware runtime still works + retrieved, err := reg.Get("nonAware1") + if err != nil { + t.Fatalf("Failed to get nonAware1 runtime: %v", err) + } + if retrieved.Type() != "nonAware1" { + t.Errorf("Expected nonAware1 runtime, got %s", retrieved.Type()) + } +}