github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/client/pluginmanager/csimanager/manager_test.go (about) 1 package csimanager 2 3 import ( 4 "fmt" 5 "sync" 6 "testing" 7 "time" 8 9 "github.com/hashicorp/nomad/client/dynamicplugins" 10 "github.com/hashicorp/nomad/client/pluginmanager" 11 "github.com/hashicorp/nomad/helper/testlog" 12 "github.com/hashicorp/nomad/nomad/structs" 13 "github.com/stretchr/testify/require" 14 ) 15 16 var _ pluginmanager.PluginManager = (*csiManager)(nil) 17 18 func fakePlugin(idx int, pluginType string) *dynamicplugins.PluginInfo { 19 id := fmt.Sprintf("alloc-%d", idx) 20 return &dynamicplugins.PluginInfo{ 21 Name: "my-plugin", 22 Type: pluginType, 23 Version: fmt.Sprintf("v%d", idx), 24 ConnectionInfo: &dynamicplugins.PluginConnectionInfo{ 25 SocketPath: "/var/data/alloc/" + id + "/csi.sock"}, 26 AllocID: id, 27 } 28 } 29 30 func testManager(t *testing.T, registry dynamicplugins.Registry, resyncPeriod time.Duration) *csiManager { 31 return New(&Config{ 32 Logger: testlog.HCLogger(t), 33 DynamicRegistry: registry, 34 UpdateNodeCSIInfoFunc: func(string, *structs.CSIInfo) {}, 35 PluginResyncPeriod: resyncPeriod, 36 }).(*csiManager) 37 } 38 39 func setupRegistry(reg *MemDB) dynamicplugins.Registry { 40 return dynamicplugins.NewRegistry( 41 reg, 42 map[string]dynamicplugins.PluginDispenser{ 43 "csi-controller": func(i *dynamicplugins.PluginInfo) (interface{}, error) { 44 return i, nil 45 }, 46 "csi-node": func(i *dynamicplugins.PluginInfo) (interface{}, error) { 47 return i, nil 48 }, 49 }) 50 } 51 52 func TestManager_RegisterPlugin(t *testing.T) { 53 registry := setupRegistry(nil) 54 defer registry.Shutdown() 55 pm := testManager(t, registry, time.Hour) 56 defer pm.Shutdown() 57 58 plugin := fakePlugin(0, dynamicplugins.PluginTypeCSIController) 59 err := registry.RegisterPlugin(plugin) 60 require.NoError(t, err) 61 62 pm.Run() 63 64 require.Eventually(t, func() bool { 65 im := instanceManagerByTypeAndName(pm, plugin.Type, plugin.Name) 66 return im != nil 67 }, 5*time.Second, 10*time.Millisecond) 68 } 69 70 func TestManager_DeregisterPlugin(t *testing.T) { 71 registry := setupRegistry(nil) 72 defer registry.Shutdown() 73 pm := testManager(t, registry, 500*time.Millisecond) 74 defer pm.Shutdown() 75 76 plugin := fakePlugin(0, dynamicplugins.PluginTypeCSIController) 77 err := registry.RegisterPlugin(plugin) 78 require.NoError(t, err) 79 80 pm.Run() 81 82 require.Eventually(t, func() bool { 83 im := instanceManagerByTypeAndName(pm, plugin.Type, plugin.Name) 84 return im != nil 85 }, 5*time.Second, 10*time.Millisecond) 86 87 err = registry.DeregisterPlugin(plugin.Type, plugin.Name, "alloc-0") 88 require.NoError(t, err) 89 90 require.Eventually(t, func() bool { 91 im := instanceManagerByTypeAndName(pm, plugin.Type, plugin.Name) 92 return im == nil 93 }, 5*time.Second, 10*time.Millisecond) 94 } 95 96 // TestManager_MultiplePlugins ensures that multiple plugins with the same 97 // name but different types (as found with monolith plugins) don't interfere 98 // with each other. 99 func TestManager_MultiplePlugins(t *testing.T) { 100 registry := setupRegistry(nil) 101 defer registry.Shutdown() 102 103 pm := testManager(t, registry, 500*time.Millisecond) 104 defer pm.Shutdown() 105 106 controllerPlugin := fakePlugin(0, dynamicplugins.PluginTypeCSIController) 107 err := registry.RegisterPlugin(controllerPlugin) 108 require.NoError(t, err) 109 110 nodePlugin := fakePlugin(0, dynamicplugins.PluginTypeCSINode) 111 err = registry.RegisterPlugin(nodePlugin) 112 require.NoError(t, err) 113 114 pm.Run() 115 116 require.Eventually(t, func() bool { 117 im := instanceManagerByTypeAndName(pm, controllerPlugin.Type, controllerPlugin.Name) 118 return im != nil 119 }, 5*time.Second, 10*time.Millisecond) 120 121 require.Eventually(t, func() bool { 122 im := instanceManagerByTypeAndName(pm, nodePlugin.Type, nodePlugin.Name) 123 return im != nil 124 }, 5*time.Second, 10*time.Millisecond) 125 126 err = registry.DeregisterPlugin(controllerPlugin.Type, controllerPlugin.Name, "alloc-0") 127 require.NoError(t, err) 128 129 require.Eventually(t, func() bool { 130 im := instanceManagerByTypeAndName(pm, controllerPlugin.Type, controllerPlugin.Name) 131 return im == nil 132 }, 5*time.Second, 10*time.Millisecond) 133 } 134 135 // TestManager_ConcurrentPlugins exercises the behavior when multiple 136 // allocations for the same plugin interact 137 func TestManager_ConcurrentPlugins(t *testing.T) { 138 139 t.Run("replacement races on host restart", func(t *testing.T) { 140 plugin0 := fakePlugin(0, dynamicplugins.PluginTypeCSINode) 141 plugin1 := fakePlugin(1, dynamicplugins.PluginTypeCSINode) 142 plugin2 := fakePlugin(2, dynamicplugins.PluginTypeCSINode) 143 144 db := &MemDB{} 145 registry := setupRegistry(db) 146 pm := testManager(t, registry, time.Hour) // no resync except from events 147 pm.Run() 148 149 require.NoError(t, registry.RegisterPlugin(plugin0)) 150 require.NoError(t, registry.RegisterPlugin(plugin1)) 151 require.Eventuallyf(t, func() bool { 152 im := instanceManagerByTypeAndName(pm, plugin0.Type, plugin0.Name) 153 return im != nil && 154 im.info.ConnectionInfo.SocketPath == "/var/data/alloc/alloc-1/csi.sock" && 155 im.allocID == "alloc-1" 156 }, 5*time.Second, 10*time.Millisecond, "alloc-1 plugin did not become active plugin") 157 158 pm.Shutdown() 159 registry.Shutdown() 160 161 // client restarts and we load state from disk. 162 // most recently inserted plugin is current 163 164 registry = setupRegistry(db) 165 defer registry.Shutdown() 166 pm = testManager(t, registry, time.Hour) 167 defer pm.Shutdown() 168 pm.Run() 169 170 require.Eventuallyf(t, func() bool { 171 im := instanceManagerByTypeAndName(pm, plugin0.Type, plugin0.Name) 172 return im != nil && 173 im.info.ConnectionInfo.SocketPath == "/var/data/alloc/alloc-1/csi.sock" && 174 im.allocID == "alloc-1" 175 }, 5*time.Second, 10*time.Millisecond, "alloc-1 plugin was not active after state reload") 176 177 // RestoreTask fires for all allocations but none of them are 178 // running because we restarted the whole host. Server gives 179 // us a replacement alloc 180 181 require.NoError(t, registry.RegisterPlugin(plugin2)) 182 require.Eventuallyf(t, func() bool { 183 im := instanceManagerByTypeAndName(pm, plugin0.Type, plugin0.Name) 184 return im != nil && 185 im.info.ConnectionInfo.SocketPath == "/var/data/alloc/alloc-2/csi.sock" && 186 im.allocID == "alloc-2" 187 }, 5*time.Second, 10*time.Millisecond, "alloc-2 plugin was not active after replacement") 188 189 }) 190 191 t.Run("interleaved register and deregister", func(t *testing.T) { 192 plugin0 := fakePlugin(0, dynamicplugins.PluginTypeCSINode) 193 plugin1 := fakePlugin(1, dynamicplugins.PluginTypeCSINode) 194 195 db := &MemDB{} 196 registry := setupRegistry(db) 197 defer registry.Shutdown() 198 199 pm := testManager(t, registry, time.Hour) // no resync except from events 200 defer pm.Shutdown() 201 pm.Run() 202 203 require.NoError(t, registry.RegisterPlugin(plugin0)) 204 require.NoError(t, registry.RegisterPlugin(plugin1)) 205 206 require.Eventuallyf(t, func() bool { 207 im := instanceManagerByTypeAndName(pm, plugin0.Type, plugin0.Name) 208 return im != nil && 209 im.info.ConnectionInfo.SocketPath == "/var/data/alloc/alloc-1/csi.sock" && 210 im.allocID == "alloc-1" 211 }, 5*time.Second, 10*time.Millisecond, "alloc-1 plugin did not become active plugin") 212 213 registry.DeregisterPlugin(dynamicplugins.PluginTypeCSINode, "my-plugin", "alloc-0") 214 215 require.Eventuallyf(t, func() bool { 216 im := instanceManagerByTypeAndName(pm, plugin0.Type, plugin0.Name) 217 return im != nil && 218 im.info.ConnectionInfo.SocketPath == "/var/data/alloc/alloc-1/csi.sock" 219 }, 5*time.Second, 10*time.Millisecond, "alloc-1 plugin should still be active plugin") 220 }) 221 } 222 223 // instanceManagerByTypeAndName is a test helper to get the instance 224 // manager for the plugin, protected by the lock that the csiManager 225 // will normally do internally 226 func instanceManagerByTypeAndName(mgr *csiManager, pluginType, pluginName string) *instanceManager { 227 mgr.instancesLock.RLock() 228 defer mgr.instancesLock.RUnlock() 229 im, _ := mgr.instances[pluginType][pluginName] 230 return im 231 } 232 233 // MemDB implements a StateDB that stores data in memory and should only be 234 // used for testing. All methods are safe for concurrent use. This is a 235 // partial implementation of the MemDB in the client/state package, copied 236 // here to avoid circular dependencies. 237 type MemDB struct { 238 dynamicManagerPs *dynamicplugins.RegistryState 239 mu sync.RWMutex 240 } 241 242 func (m *MemDB) GetDynamicPluginRegistryState() (*dynamicplugins.RegistryState, error) { 243 if m == nil { 244 return nil, nil 245 } 246 m.mu.Lock() 247 defer m.mu.Unlock() 248 return m.dynamicManagerPs, nil 249 } 250 251 func (m *MemDB) PutDynamicPluginRegistryState(ps *dynamicplugins.RegistryState) error { 252 if m == nil { 253 return nil 254 } 255 m.mu.Lock() 256 defer m.mu.Unlock() 257 m.dynamicManagerPs = ps 258 return nil 259 }