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  }