github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/client/dynamicplugins/registry_test.go (about) 1 package dynamicplugins 2 3 import ( 4 "context" 5 "fmt" 6 "sync" 7 "testing" 8 "time" 9 10 "github.com/hashicorp/nomad/ci" 11 "github.com/stretchr/testify/require" 12 ) 13 14 func TestPluginEventBroadcaster_SendsMessagesToAllClients(t *testing.T) { 15 ci.Parallel(t) 16 17 b := newPluginEventBroadcaster() 18 defer close(b.stopCh) 19 20 var rcv1, rcv2 bool 21 ch1 := b.subscribe() 22 ch2 := b.subscribe() 23 24 var wg sync.WaitGroup 25 wg.Add(1) 26 ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 27 defer cancel() 28 29 go func() { 30 defer wg.Done() 31 for { 32 select { 33 case <-ctx.Done(): 34 t.Errorf("did not receive event on both subscriptions before timeout") 35 return 36 case <-ch1: 37 rcv1 = true 38 case <-ch2: 39 rcv2 = true 40 } 41 if rcv1 && rcv2 { 42 return 43 } 44 } 45 }() 46 47 b.broadcast(&PluginUpdateEvent{}) 48 wg.Wait() 49 } 50 51 func TestPluginEventBroadcaster_UnsubscribeWorks(t *testing.T) { 52 ci.Parallel(t) 53 54 b := newPluginEventBroadcaster() 55 defer close(b.stopCh) 56 57 ch1 := b.subscribe() 58 59 var wg sync.WaitGroup 60 wg.Add(1) 61 ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 62 defer cancel() 63 64 go func() { 65 defer wg.Done() 66 for { 67 select { 68 case <-ctx.Done(): 69 t.Errorf("did not receive unsubscribe event on subscription before timeout") 70 return 71 case <-ch1: 72 return // done! 73 } 74 } 75 }() 76 77 b.unsubscribe(ch1) 78 b.broadcast(&PluginUpdateEvent{}) 79 wg.Wait() 80 } 81 82 func TestDynamicRegistry_RegisterPlugin_SendsUpdateEvents(t *testing.T) { 83 ci.Parallel(t) 84 85 r := NewRegistry(nil, nil) 86 87 var wg sync.WaitGroup 88 wg.Add(1) 89 ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 90 defer cancel() 91 92 ch := r.PluginsUpdatedCh(ctx, "csi") 93 94 go func() { 95 defer wg.Done() 96 for { 97 select { 98 case <-ctx.Done(): 99 t.Errorf("did not receive registration event on subscription before timeout") 100 return 101 case e := <-ch: 102 if e != nil && e.EventType == EventTypeRegistered { 103 return 104 } 105 } 106 } 107 }() 108 109 err := r.RegisterPlugin(&PluginInfo{ 110 Type: "csi", 111 Name: "my-plugin", 112 ConnectionInfo: &PluginConnectionInfo{}, 113 }) 114 115 require.NoError(t, err) 116 wg.Wait() 117 } 118 119 func TestDynamicRegistry_DeregisterPlugin_SendsUpdateEvents(t *testing.T) { 120 ci.Parallel(t) 121 122 r := NewRegistry(nil, nil) 123 124 var wg sync.WaitGroup 125 wg.Add(1) 126 ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 127 defer cancel() 128 129 ch := r.PluginsUpdatedCh(ctx, "csi") 130 131 go func() { 132 defer wg.Done() 133 for { 134 select { 135 case <-ctx.Done(): 136 t.Errorf("did not receive deregistration event on subscription before timeout") 137 return 138 case e := <-ch: 139 if e != nil && e.EventType == EventTypeDeregistered { 140 return 141 } 142 } 143 } 144 }() 145 146 err := r.RegisterPlugin(&PluginInfo{ 147 Type: "csi", 148 Name: "my-plugin", 149 AllocID: "alloc-0", 150 ConnectionInfo: &PluginConnectionInfo{}, 151 }) 152 require.NoError(t, err) 153 154 err = r.DeregisterPlugin("csi", "my-plugin", "alloc-0") 155 require.NoError(t, err) 156 wg.Wait() 157 } 158 159 func TestDynamicRegistry_DispensePlugin_Works(t *testing.T) { 160 ci.Parallel(t) 161 162 dispenseFn := func(i *PluginInfo) (interface{}, error) { 163 return struct{}{}, nil 164 } 165 166 registry := NewRegistry(nil, map[string]PluginDispenser{"csi": dispenseFn}) 167 168 err := registry.RegisterPlugin(&PluginInfo{ 169 Type: "csi", 170 Name: "my-plugin", 171 ConnectionInfo: &PluginConnectionInfo{}, 172 }) 173 require.NoError(t, err) 174 175 result, err := registry.DispensePlugin("unknown-type", "unknown-name") 176 require.Nil(t, result) 177 require.EqualError(t, err, "no plugin dispenser found for type: unknown-type") 178 179 result, err = registry.DispensePlugin("csi", "unknown-name") 180 require.Nil(t, result) 181 require.EqualError(t, err, "plugin unknown-name for type csi not found") 182 183 result, err = registry.DispensePlugin("csi", "my-plugin") 184 require.NotNil(t, result) 185 require.NoError(t, err) 186 } 187 188 func TestDynamicRegistry_IsolatePluginTypes(t *testing.T) { 189 ci.Parallel(t) 190 191 r := NewRegistry(nil, nil) 192 193 err := r.RegisterPlugin(&PluginInfo{ 194 Type: PluginTypeCSIController, 195 Name: "my-plugin", 196 AllocID: "alloc-0", 197 ConnectionInfo: &PluginConnectionInfo{}, 198 }) 199 require.NoError(t, err) 200 201 err = r.RegisterPlugin(&PluginInfo{ 202 Type: PluginTypeCSINode, 203 Name: "my-plugin", 204 AllocID: "alloc-1", 205 ConnectionInfo: &PluginConnectionInfo{}, 206 }) 207 require.NoError(t, err) 208 209 err = r.DeregisterPlugin(PluginTypeCSIController, "my-plugin", "alloc-0") 210 require.NoError(t, err) 211 require.Equal(t, 1, len(r.ListPlugins(PluginTypeCSINode))) 212 require.Equal(t, 0, len(r.ListPlugins(PluginTypeCSIController))) 213 } 214 215 func TestDynamicRegistry_StateStore(t *testing.T) { 216 ci.Parallel(t) 217 218 dispenseFn := func(i *PluginInfo) (interface{}, error) { 219 return i, nil 220 } 221 222 memdb := &MemDB{} 223 oldR := NewRegistry(memdb, map[string]PluginDispenser{"csi": dispenseFn}) 224 225 err := oldR.RegisterPlugin(&PluginInfo{ 226 Type: "csi", 227 Name: "my-plugin", 228 ConnectionInfo: &PluginConnectionInfo{}, 229 }) 230 require.NoError(t, err) 231 result, err := oldR.DispensePlugin("csi", "my-plugin") 232 require.NotNil(t, result) 233 require.NoError(t, err) 234 235 // recreate the registry from the state store and query again 236 newR := NewRegistry(memdb, map[string]PluginDispenser{"csi": dispenseFn}) 237 result, err = newR.DispensePlugin("csi", "my-plugin") 238 require.NotNil(t, result) 239 require.NoError(t, err) 240 } 241 242 func TestDynamicRegistry_ConcurrentAllocs(t *testing.T) { 243 ci.Parallel(t) 244 245 dispenseFn := func(i *PluginInfo) (interface{}, error) { 246 return i, nil 247 } 248 249 newPlugin := func(idx int) *PluginInfo { 250 id := fmt.Sprintf("alloc-%d", idx) 251 return &PluginInfo{ 252 Name: "my-plugin", 253 Type: PluginTypeCSINode, 254 Version: fmt.Sprintf("v%d", idx), 255 ConnectionInfo: &PluginConnectionInfo{ 256 SocketPath: "/var/data/alloc/" + id + "/csi.sock"}, 257 AllocID: id, 258 } 259 } 260 261 dispensePlugin := func(t *testing.T, reg Registry) *PluginInfo { 262 result, err := reg.DispensePlugin(PluginTypeCSINode, "my-plugin") 263 require.NotNil(t, result) 264 require.NoError(t, err) 265 plugin := result.(*PluginInfo) 266 return plugin 267 } 268 269 t.Run("restore races on client restart", func(t *testing.T) { 270 plugin0 := newPlugin(0) 271 plugin1 := newPlugin(1) 272 273 memdb := &MemDB{} 274 oldR := NewRegistry(memdb, map[string]PluginDispenser{PluginTypeCSINode: dispenseFn}) 275 276 // add a plugin and a new alloc running the same plugin 277 // (without stopping the old one) 278 require.NoError(t, oldR.RegisterPlugin(plugin0)) 279 require.NoError(t, oldR.RegisterPlugin(plugin1)) 280 plugin := dispensePlugin(t, oldR) 281 require.Equal(t, "alloc-1", plugin.AllocID) 282 283 // client restarts and we load state from disk. 284 // most recently inserted plugin is current 285 286 newR := NewRegistry(memdb, map[string]PluginDispenser{PluginTypeCSINode: dispenseFn}) 287 plugin = dispensePlugin(t, oldR) 288 require.Equal(t, "/var/data/alloc/alloc-1/csi.sock", plugin.ConnectionInfo.SocketPath) 289 require.Equal(t, "alloc-1", plugin.AllocID) 290 291 // RestoreTask fires for all allocations, which runs the 292 // plugin_supervisor_hook. But there's a race and the allocations 293 // in this scenario are Restored in the opposite order they were 294 // created 295 296 require.NoError(t, newR.RegisterPlugin(plugin0)) 297 plugin = dispensePlugin(t, newR) 298 require.Equal(t, "/var/data/alloc/alloc-1/csi.sock", plugin.ConnectionInfo.SocketPath) 299 require.Equal(t, "alloc-1", plugin.AllocID) 300 }) 301 302 t.Run("replacement races on host restart", func(t *testing.T) { 303 plugin0 := newPlugin(0) 304 plugin1 := newPlugin(1) 305 plugin2 := newPlugin(2) 306 307 memdb := &MemDB{} 308 oldR := NewRegistry(memdb, map[string]PluginDispenser{PluginTypeCSINode: dispenseFn}) 309 310 // add a plugin and a new alloc running the same plugin 311 // (without stopping the old one) 312 require.NoError(t, oldR.RegisterPlugin(plugin0)) 313 require.NoError(t, oldR.RegisterPlugin(plugin1)) 314 plugin := dispensePlugin(t, oldR) 315 require.Equal(t, "alloc-1", plugin.AllocID) 316 317 // client restarts and we load state from disk. 318 // most recently inserted plugin is current 319 320 newR := NewRegistry(memdb, map[string]PluginDispenser{PluginTypeCSINode: dispenseFn}) 321 plugin = dispensePlugin(t, oldR) 322 require.Equal(t, "/var/data/alloc/alloc-1/csi.sock", plugin.ConnectionInfo.SocketPath) 323 require.Equal(t, "alloc-1", plugin.AllocID) 324 325 // RestoreTask fires for all allocations but none of them are 326 // running because we restarted the whole host. Server gives 327 // us a replacement alloc 328 329 require.NoError(t, newR.RegisterPlugin(plugin2)) 330 plugin = dispensePlugin(t, newR) 331 require.Equal(t, "/var/data/alloc/alloc-2/csi.sock", plugin.ConnectionInfo.SocketPath) 332 require.Equal(t, "alloc-2", plugin.AllocID) 333 }) 334 335 t.Run("interleaved register and deregister", func(t *testing.T) { 336 plugin0 := newPlugin(0) 337 plugin1 := newPlugin(1) 338 339 memdb := &MemDB{} 340 reg := NewRegistry(memdb, map[string]PluginDispenser{PluginTypeCSINode: dispenseFn}) 341 342 require.NoError(t, reg.RegisterPlugin(plugin0)) 343 344 // replacement is registered before old plugin deregisters 345 require.NoError(t, reg.RegisterPlugin(plugin1)) 346 plugin := dispensePlugin(t, reg) 347 require.Equal(t, "alloc-1", plugin.AllocID) 348 349 reg.DeregisterPlugin(PluginTypeCSINode, "my-plugin", "alloc-0") 350 plugin = dispensePlugin(t, reg) 351 require.Equal(t, "alloc-1", plugin.AllocID) 352 }) 353 354 } 355 356 // MemDB implements a StateDB that stores data in memory and should only be 357 // used for testing. All methods are safe for concurrent use. This is a 358 // partial implementation of the MemDB in the client/state package, copied 359 // here to avoid circular dependencies. 360 type MemDB struct { 361 dynamicManagerPs *RegistryState 362 mu sync.RWMutex 363 } 364 365 func (m *MemDB) GetDynamicPluginRegistryState() (*RegistryState, error) { 366 m.mu.Lock() 367 defer m.mu.Unlock() 368 return m.dynamicManagerPs, nil 369 } 370 371 func (m *MemDB) PutDynamicPluginRegistryState(ps *RegistryState) error { 372 m.mu.Lock() 373 defer m.mu.Unlock() 374 m.dynamicManagerPs = ps 375 return nil 376 }