github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/backend/remote-state/consul/client_test.go (about)

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