github.com/iaas-resource-provision/iaas-rpc@v1.0.7-0.20211021023331-ed21f798c408/internal/backend/remote-state/consul/client_test.go (about)

     1  package consul
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"math/rand"
     9  	"net"
    10  	"reflect"
    11  	"strings"
    12  	"sync"
    13  	"testing"
    14  	"time"
    15  
    16  	"github.com/iaas-resource-provision/iaas-rpc/internal/backend"
    17  	"github.com/iaas-resource-provision/iaas-rpc/internal/states/remote"
    18  	"github.com/iaas-resource-provision/iaas-rpc/internal/states/statemgr"
    19  )
    20  
    21  func TestRemoteClient_impl(t *testing.T) {
    22  	var _ remote.Client = new(RemoteClient)
    23  	var _ remote.ClientLocker = new(RemoteClient)
    24  }
    25  
    26  func TestRemoteClient(t *testing.T) {
    27  	testCases := []string{
    28  		fmt.Sprintf("tf-unit/%s", time.Now().String()),
    29  		fmt.Sprintf("tf-unit/%s/", time.Now().String()),
    30  	}
    31  
    32  	for _, path := range testCases {
    33  		t.Run(path, func(*testing.T) {
    34  			// Get the backend
    35  			b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
    36  				"address": srv.HTTPAddr,
    37  				"path":    path,
    38  			}))
    39  
    40  			// Grab the client
    41  			state, err := b.StateMgr(backend.DefaultStateName)
    42  			if err != nil {
    43  				t.Fatalf("err: %s", err)
    44  			}
    45  
    46  			// Test
    47  			remote.TestClient(t, state.(*remote.State).Client)
    48  		})
    49  	}
    50  }
    51  
    52  // test the gzip functionality of the client
    53  func TestRemoteClient_gzipUpgrade(t *testing.T) {
    54  	statePath := fmt.Sprintf("tf-unit/%s", time.Now().String())
    55  
    56  	// Get the backend
    57  	b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
    58  		"address": srv.HTTPAddr,
    59  		"path":    statePath,
    60  	}))
    61  
    62  	// Grab the client
    63  	state, err := b.StateMgr(backend.DefaultStateName)
    64  	if err != nil {
    65  		t.Fatalf("err: %s", err)
    66  	}
    67  
    68  	// Test
    69  	remote.TestClient(t, state.(*remote.State).Client)
    70  
    71  	// create a new backend with gzip
    72  	b = backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
    73  		"address": srv.HTTPAddr,
    74  		"path":    statePath,
    75  		"gzip":    true,
    76  	}))
    77  
    78  	// Grab the client
    79  	state, err = b.StateMgr(backend.DefaultStateName)
    80  	if err != nil {
    81  		t.Fatalf("err: %s", err)
    82  	}
    83  
    84  	// Test
    85  	remote.TestClient(t, state.(*remote.State).Client)
    86  }
    87  
    88  // TestConsul_largeState tries to write a large payload using the Consul state
    89  // manager, as there is a limit to the size of the values in the KV store it
    90  // will need to be split up before being saved and put back together when read.
    91  func TestConsul_largeState(t *testing.T) {
    92  	path := "tf-unit/test-large-state"
    93  
    94  	b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
    95  		"address": srv.HTTPAddr,
    96  		"path":    path,
    97  	}))
    98  
    99  	s, err := b.StateMgr(backend.DefaultStateName)
   100  	if err != nil {
   101  		t.Fatal(err)
   102  	}
   103  
   104  	c := s.(*remote.State).Client.(*RemoteClient)
   105  	c.Path = path
   106  
   107  	// testPaths fails the test if the keys found at the prefix don't match
   108  	// what is expected
   109  	testPaths := func(t *testing.T, expected []string) {
   110  		kv := c.Client.KV()
   111  		pairs, _, err := kv.List(c.Path, nil)
   112  		if err != nil {
   113  			t.Fatal(err)
   114  		}
   115  		res := make([]string, 0)
   116  		for _, p := range pairs {
   117  			res = append(res, p.Key)
   118  		}
   119  		if !reflect.DeepEqual(res, expected) {
   120  			t.Fatalf("Wrong keys: %#v", res)
   121  		}
   122  	}
   123  
   124  	testPayload := func(t *testing.T, data map[string]string, keys []string) {
   125  		payload, err := json.Marshal(data)
   126  		if err != nil {
   127  			t.Fatal(err)
   128  		}
   129  		err = c.Put(payload)
   130  		if err != nil {
   131  			t.Fatal("could not put payload", err)
   132  		}
   133  
   134  		remote, err := c.Get()
   135  		if err != nil {
   136  			t.Fatal(err)
   137  		}
   138  
   139  		if !bytes.Equal(payload, remote.Data) {
   140  			t.Fatal("the data do not match")
   141  		}
   142  
   143  		testPaths(t, keys)
   144  	}
   145  
   146  	// The default limit for the size of the value in Consul is 524288 bytes
   147  	testPayload(
   148  		t,
   149  		map[string]string{
   150  			"foo": strings.Repeat("a", 524288+2),
   151  		},
   152  		[]string{
   153  			"tf-unit/test-large-state",
   154  			"tf-unit/test-large-state/tfstate.2cb96f52c9fff8e0b56cb786ec4d2bed/0",
   155  			"tf-unit/test-large-state/tfstate.2cb96f52c9fff8e0b56cb786ec4d2bed/1",
   156  		},
   157  	)
   158  
   159  	// This payload is just short enough to be stored but will be bigger when
   160  	// going through the Transaction API as it will be base64 encoded
   161  	testPayload(
   162  		t,
   163  		map[string]string{
   164  			"foo": strings.Repeat("a", 524288-10),
   165  		},
   166  		[]string{
   167  			"tf-unit/test-large-state",
   168  			"tf-unit/test-large-state/tfstate.4f407ace136a86521fd0d366972fe5c7/0",
   169  		},
   170  	)
   171  
   172  	// We try to replace the payload with a small one, the old chunks should be removed
   173  	testPayload(
   174  		t,
   175  		map[string]string{"var": "a"},
   176  		[]string{"tf-unit/test-large-state"},
   177  	)
   178  
   179  	// Test with gzip and chunks
   180  	b = backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
   181  		"address": srv.HTTPAddr,
   182  		"path":    path,
   183  		"gzip":    true,
   184  	}))
   185  
   186  	s, err = b.StateMgr(backend.DefaultStateName)
   187  	if err != nil {
   188  		t.Fatal(err)
   189  	}
   190  
   191  	c = s.(*remote.State).Client.(*RemoteClient)
   192  	c.Path = path
   193  
   194  	// We need a long random string so it results in multiple chunks even after
   195  	// being gziped
   196  
   197  	// We use a fixed seed so the test can be reproductible
   198  	rand.Seed(1234)
   199  	RandStringRunes := func(n int) string {
   200  		var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
   201  		b := make([]rune, n)
   202  		for i := range b {
   203  			b[i] = letterRunes[rand.Intn(len(letterRunes))]
   204  		}
   205  		return string(b)
   206  	}
   207  
   208  	testPayload(
   209  		t,
   210  		map[string]string{
   211  			"bar": RandStringRunes(5 * (524288 + 2)),
   212  		},
   213  		[]string{
   214  			"tf-unit/test-large-state",
   215  			"tf-unit/test-large-state/tfstate.58e8160335864b520b1cc7f2222a4019/0",
   216  			"tf-unit/test-large-state/tfstate.58e8160335864b520b1cc7f2222a4019/1",
   217  			"tf-unit/test-large-state/tfstate.58e8160335864b520b1cc7f2222a4019/2",
   218  			"tf-unit/test-large-state/tfstate.58e8160335864b520b1cc7f2222a4019/3",
   219  		},
   220  	)
   221  
   222  	// Deleting the state should remove all chunks
   223  	err = c.Delete()
   224  	if err != nil {
   225  		t.Fatal(err)
   226  	}
   227  	testPaths(t, []string{})
   228  }
   229  
   230  func TestConsul_stateLock(t *testing.T) {
   231  	testCases := []string{
   232  		fmt.Sprintf("tf-unit/%s", time.Now().String()),
   233  		fmt.Sprintf("tf-unit/%s/", time.Now().String()),
   234  	}
   235  
   236  	for _, path := range testCases {
   237  		t.Run(path, func(*testing.T) {
   238  			// create 2 instances to get 2 remote.Clients
   239  			sA, err := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
   240  				"address": srv.HTTPAddr,
   241  				"path":    path,
   242  			})).StateMgr(backend.DefaultStateName)
   243  			if err != nil {
   244  				t.Fatal(err)
   245  			}
   246  
   247  			sB, err := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
   248  				"address": srv.HTTPAddr,
   249  				"path":    path,
   250  			})).StateMgr(backend.DefaultStateName)
   251  			if err != nil {
   252  				t.Fatal(err)
   253  			}
   254  
   255  			remote.TestRemoteLocks(t, sA.(*remote.State).Client, sB.(*remote.State).Client)
   256  		})
   257  	}
   258  }
   259  
   260  func TestConsul_destroyLock(t *testing.T) {
   261  	testCases := []string{
   262  		fmt.Sprintf("tf-unit/%s", time.Now().String()),
   263  		fmt.Sprintf("tf-unit/%s/", time.Now().String()),
   264  	}
   265  
   266  	testLock := func(client *RemoteClient, lockPath string) {
   267  		// get the lock val
   268  		pair, _, err := client.Client.KV().Get(lockPath, nil)
   269  		if err != nil {
   270  			t.Fatal(err)
   271  		}
   272  		if pair != nil {
   273  			t.Fatalf("lock key not cleaned up at: %s", pair.Key)
   274  		}
   275  	}
   276  
   277  	for _, path := range testCases {
   278  		t.Run(path, func(*testing.T) {
   279  			// Get the backend
   280  			b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
   281  				"address": srv.HTTPAddr,
   282  				"path":    path,
   283  			}))
   284  
   285  			// Grab the client
   286  			s, err := b.StateMgr(backend.DefaultStateName)
   287  			if err != nil {
   288  				t.Fatalf("err: %s", err)
   289  			}
   290  
   291  			clientA := s.(*remote.State).Client.(*RemoteClient)
   292  
   293  			info := statemgr.NewLockInfo()
   294  			id, err := clientA.Lock(info)
   295  			if err != nil {
   296  				t.Fatal(err)
   297  			}
   298  
   299  			lockPath := clientA.Path + lockSuffix
   300  
   301  			if err := clientA.Unlock(id); err != nil {
   302  				t.Fatal(err)
   303  			}
   304  
   305  			testLock(clientA, lockPath)
   306  
   307  			// The release the lock from a second client to test the
   308  			// `terraform force-unlock <lock_id>` functionnality
   309  			s, err = b.StateMgr(backend.DefaultStateName)
   310  			if err != nil {
   311  				t.Fatalf("err: %s", err)
   312  			}
   313  
   314  			clientB := s.(*remote.State).Client.(*RemoteClient)
   315  
   316  			info = statemgr.NewLockInfo()
   317  			id, err = clientA.Lock(info)
   318  			if err != nil {
   319  				t.Fatal(err)
   320  			}
   321  
   322  			if err := clientB.Unlock(id); err != nil {
   323  				t.Fatal(err)
   324  			}
   325  
   326  			testLock(clientA, lockPath)
   327  
   328  			err = clientA.Unlock(id)
   329  
   330  			if err == nil {
   331  				t.Fatal("consul lock should have been lost")
   332  			}
   333  			if err.Error() != "consul lock was lost" {
   334  				t.Fatal("got wrong error", err)
   335  			}
   336  		})
   337  	}
   338  }
   339  
   340  func TestConsul_lostLock(t *testing.T) {
   341  	path := fmt.Sprintf("tf-unit/%s", time.Now().String())
   342  
   343  	// create 2 instances to get 2 remote.Clients
   344  	sA, err := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
   345  		"address": srv.HTTPAddr,
   346  		"path":    path,
   347  	})).StateMgr(backend.DefaultStateName)
   348  	if err != nil {
   349  		t.Fatal(err)
   350  	}
   351  
   352  	sB, err := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
   353  		"address": srv.HTTPAddr,
   354  		"path":    path + "-not-used",
   355  	})).StateMgr(backend.DefaultStateName)
   356  	if err != nil {
   357  		t.Fatal(err)
   358  	}
   359  
   360  	info := statemgr.NewLockInfo()
   361  	info.Operation = "test-lost-lock"
   362  	id, err := sA.Lock(info)
   363  	if err != nil {
   364  		t.Fatal(err)
   365  	}
   366  
   367  	reLocked := make(chan struct{})
   368  	testLockHook = func() {
   369  		close(reLocked)
   370  		testLockHook = nil
   371  	}
   372  
   373  	// now we use the second client to break the lock
   374  	kv := sB.(*remote.State).Client.(*RemoteClient).Client.KV()
   375  	_, err = kv.Delete(path+lockSuffix, nil)
   376  	if err != nil {
   377  		t.Fatal(err)
   378  	}
   379  
   380  	<-reLocked
   381  
   382  	if err := sA.Unlock(id); err != nil {
   383  		t.Fatal(err)
   384  	}
   385  }
   386  
   387  func TestConsul_lostLockConnection(t *testing.T) {
   388  	// create an "unreliable" network by closing all the consul client's
   389  	// network connections
   390  	conns := &unreliableConns{}
   391  	origDialFn := dialContext
   392  	defer func() {
   393  		dialContext = origDialFn
   394  	}()
   395  	dialContext = conns.DialContext
   396  
   397  	path := fmt.Sprintf("tf-unit/%s", time.Now().String())
   398  
   399  	b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
   400  		"address": srv.HTTPAddr,
   401  		"path":    path,
   402  	}))
   403  
   404  	s, err := b.StateMgr(backend.DefaultStateName)
   405  	if err != nil {
   406  		t.Fatal(err)
   407  	}
   408  
   409  	info := statemgr.NewLockInfo()
   410  	info.Operation = "test-lost-lock-connection"
   411  	id, err := s.Lock(info)
   412  	if err != nil {
   413  		t.Fatal(err)
   414  	}
   415  
   416  	// kill the connection a few times
   417  	for i := 0; i < 3; i++ {
   418  		dialed := conns.dialedDone()
   419  		// kill any open connections
   420  		conns.Kill()
   421  		// wait for a new connection to be dialed, and kill it again
   422  		<-dialed
   423  	}
   424  
   425  	if err := s.Unlock(id); err != nil {
   426  		t.Fatal("unlock error:", err)
   427  	}
   428  }
   429  
   430  type unreliableConns struct {
   431  	sync.Mutex
   432  	conns        []net.Conn
   433  	dialCallback func()
   434  }
   435  
   436  func (u *unreliableConns) DialContext(ctx context.Context, netw, addr string) (net.Conn, error) {
   437  	u.Lock()
   438  	defer u.Unlock()
   439  
   440  	dialer := &net.Dialer{}
   441  	conn, err := dialer.DialContext(ctx, netw, addr)
   442  	if err != nil {
   443  		return nil, err
   444  	}
   445  
   446  	u.conns = append(u.conns, conn)
   447  
   448  	if u.dialCallback != nil {
   449  		u.dialCallback()
   450  	}
   451  
   452  	return conn, nil
   453  }
   454  
   455  func (u *unreliableConns) dialedDone() chan struct{} {
   456  	u.Lock()
   457  	defer u.Unlock()
   458  	dialed := make(chan struct{})
   459  	u.dialCallback = func() {
   460  		defer close(dialed)
   461  		u.dialCallback = nil
   462  	}
   463  
   464  	return dialed
   465  }
   466  
   467  // Kill these with a deadline, just to make sure we don't end up with any EOFs
   468  // that get ignored.
   469  func (u *unreliableConns) Kill() {
   470  	u.Lock()
   471  	defer u.Unlock()
   472  
   473  	for _, conn := range u.conns {
   474  		conn.(*net.TCPConn).SetDeadline(time.Now())
   475  	}
   476  	u.conns = nil
   477  }