github.imxd.top/hashicorp/consul@v1.4.5/agent/proxycfg/testing.go (about)

     1  package proxycfg
     2  
     3  import (
     4  	"sync"
     5  	"sync/atomic"
     6  	"time"
     7  
     8  	"github.com/hashicorp/consul/agent/cache"
     9  	cachetype "github.com/hashicorp/consul/agent/cache-types"
    10  	"github.com/hashicorp/consul/agent/connect"
    11  	"github.com/hashicorp/consul/agent/structs"
    12  	"github.com/mitchellh/go-testing-interface"
    13  	"github.com/stretchr/testify/require"
    14  )
    15  
    16  // TestCacheTypes encapsulates all the different cache types proxycfg.State will
    17  // watch/request for controlling one during testing.
    18  type TestCacheTypes struct {
    19  	roots      *ControllableCacheType
    20  	leaf       *ControllableCacheType
    21  	intentions *ControllableCacheType
    22  	health     *ControllableCacheType
    23  	query      *ControllableCacheType
    24  }
    25  
    26  // NewTestCacheTypes creates a set of ControllableCacheTypes for all types that
    27  // proxycfg will watch suitable for testing a proxycfg.State or Manager.
    28  func NewTestCacheTypes(t testing.T) *TestCacheTypes {
    29  	t.Helper()
    30  	ct := &TestCacheTypes{
    31  		roots:      NewControllableCacheType(t),
    32  		leaf:       NewControllableCacheType(t),
    33  		intentions: NewControllableCacheType(t),
    34  		health:     NewControllableCacheType(t),
    35  		query:      NewControllableCacheType(t),
    36  	}
    37  	ct.query.blocking = false
    38  	return ct
    39  }
    40  
    41  // TestCacheWithTypes registers ControllableCacheTypes for all types that
    42  // proxycfg will watch suitable for testing a proxycfg.State or Manager.
    43  func TestCacheWithTypes(t testing.T, types *TestCacheTypes) *cache.Cache {
    44  	c := cache.TestCache(t)
    45  	c.RegisterType(cachetype.ConnectCARootName, types.roots, &cache.RegisterOptions{
    46  		Refresh:        true,
    47  		RefreshTimer:   0,
    48  		RefreshTimeout: 10 * time.Minute,
    49  	})
    50  	c.RegisterType(cachetype.ConnectCALeafName, types.leaf, &cache.RegisterOptions{
    51  		Refresh:        true,
    52  		RefreshTimer:   0,
    53  		RefreshTimeout: 10 * time.Minute,
    54  	})
    55  	c.RegisterType(cachetype.IntentionMatchName, types.intentions, &cache.RegisterOptions{
    56  		Refresh:        true,
    57  		RefreshTimer:   0,
    58  		RefreshTimeout: 10 * time.Minute,
    59  	})
    60  	c.RegisterType(cachetype.HealthServicesName, types.health, &cache.RegisterOptions{
    61  		Refresh:        true,
    62  		RefreshTimer:   0,
    63  		RefreshTimeout: 10 * time.Minute,
    64  	})
    65  	c.RegisterType(cachetype.PreparedQueryName, types.query, &cache.RegisterOptions{
    66  		Refresh: false,
    67  	})
    68  	return c
    69  }
    70  
    71  // TestCerts generates a CA and Leaf suitable for returning as mock CA
    72  // root/leaf cache requests.
    73  func TestCerts(t testing.T) (*structs.IndexedCARoots, *structs.IssuedCert) {
    74  	t.Helper()
    75  
    76  	ca := connect.TestCA(t, nil)
    77  	roots := &structs.IndexedCARoots{
    78  		ActiveRootID: ca.ID,
    79  		TrustDomain:  connect.TestClusterID,
    80  		Roots:        []*structs.CARoot{ca},
    81  	}
    82  	return roots, TestLeafForCA(t, ca)
    83  }
    84  
    85  // TestLeafForCA generates new Leaf suitable for returning as mock CA
    86  // leaf cache response, signed by an existing CA.
    87  func TestLeafForCA(t testing.T, ca *structs.CARoot) *structs.IssuedCert {
    88  	leafPEM, pkPEM := connect.TestLeaf(t, "web", ca)
    89  
    90  	leafCert, err := connect.ParseCert(leafPEM)
    91  	require.NoError(t, err)
    92  
    93  	return &structs.IssuedCert{
    94  		SerialNumber:  connect.HexString(leafCert.SerialNumber.Bytes()),
    95  		CertPEM:       leafPEM,
    96  		PrivateKeyPEM: pkPEM,
    97  		Service:       "web",
    98  		ServiceURI:    leafCert.URIs[0].String(),
    99  		ValidAfter:    leafCert.NotBefore,
   100  		ValidBefore:   leafCert.NotAfter,
   101  	}
   102  }
   103  
   104  // TestIntentions returns a sample intentions match result useful to
   105  // mocking service discovery cache results.
   106  func TestIntentions(t testing.T) *structs.IndexedIntentionMatches {
   107  	return &structs.IndexedIntentionMatches{
   108  		Matches: []structs.Intentions{
   109  			[]*structs.Intention{
   110  				&structs.Intention{
   111  					ID:              "foo",
   112  					SourceNS:        "default",
   113  					SourceName:      "billing",
   114  					DestinationNS:   "default",
   115  					DestinationName: "web",
   116  					Action:          structs.IntentionActionAllow,
   117  				},
   118  			},
   119  		},
   120  	}
   121  }
   122  
   123  // TestUpstreamNodes returns a sample service discovery result useful to
   124  // mocking service discovery cache results.
   125  func TestUpstreamNodes(t testing.T) structs.CheckServiceNodes {
   126  	return structs.CheckServiceNodes{
   127  		structs.CheckServiceNode{
   128  			Node: &structs.Node{
   129  				ID:         "test1",
   130  				Node:       "test1",
   131  				Address:    "10.10.1.1",
   132  				Datacenter: "dc1",
   133  			},
   134  			Service: structs.TestNodeService(t),
   135  		},
   136  		structs.CheckServiceNode{
   137  			Node: &structs.Node{
   138  				ID:         "test2",
   139  				Node:       "test2",
   140  				Address:    "10.10.1.2",
   141  				Datacenter: "dc1",
   142  			},
   143  			Service: structs.TestNodeService(t),
   144  		},
   145  	}
   146  }
   147  
   148  // TestConfigSnapshot returns a fully populated snapshot
   149  func TestConfigSnapshot(t testing.T) *ConfigSnapshot {
   150  	roots, leaf := TestCerts(t)
   151  	return &ConfigSnapshot{
   152  		ProxyID: "web-sidecar-proxy",
   153  		Address: "0.0.0.0",
   154  		Port:    9999,
   155  		Proxy: structs.ConnectProxyConfig{
   156  			DestinationServiceID:   "web",
   157  			DestinationServiceName: "web",
   158  			LocalServiceAddress:    "127.0.0.1",
   159  			LocalServicePort:       8080,
   160  			Config: map[string]interface{}{
   161  				"foo": "bar",
   162  			},
   163  			Upstreams: structs.TestUpstreams(t),
   164  		},
   165  		Roots: roots,
   166  		Leaf:  leaf,
   167  		UpstreamEndpoints: map[string]structs.CheckServiceNodes{
   168  			"service:db": TestUpstreamNodes(t),
   169  		},
   170  	}
   171  }
   172  
   173  // ControllableCacheType is a cache.Type that simulates a typical blocking RPC
   174  // but lets us control the responses and when they are delivered easily.
   175  type ControllableCacheType struct {
   176  	index uint64
   177  	value atomic.Value
   178  	// Need a condvar to trigger all blocking requests (there might be multiple
   179  	// for same type due to background refresh and timing issues) when values
   180  	// change. Chans make it nondeterministic which one triggers or need extra
   181  	// locking to coordinate replacing after close etc.
   182  	triggerMu sync.Mutex
   183  	trigger   *sync.Cond
   184  	blocking  bool
   185  	lastReq   atomic.Value
   186  }
   187  
   188  // NewControllableCacheType returns a cache.Type that can be controlled for
   189  // testing.
   190  func NewControllableCacheType(t testing.T) *ControllableCacheType {
   191  	c := &ControllableCacheType{
   192  		index:    5,
   193  		blocking: true,
   194  	}
   195  	c.trigger = sync.NewCond(&c.triggerMu)
   196  	return c
   197  }
   198  
   199  // Set sets the response value to be returned from subsequent cache gets for the
   200  // type.
   201  func (ct *ControllableCacheType) Set(value interface{}) {
   202  	atomic.AddUint64(&ct.index, 1)
   203  	ct.value.Store(value)
   204  	ct.triggerMu.Lock()
   205  	ct.trigger.Broadcast()
   206  	ct.triggerMu.Unlock()
   207  }
   208  
   209  // Fetch implements cache.Type. It simulates blocking or non-blocking queries.
   210  func (ct *ControllableCacheType) Fetch(opts cache.FetchOptions, req cache.Request) (cache.FetchResult, error) {
   211  
   212  	index := atomic.LoadUint64(&ct.index)
   213  
   214  	ct.lastReq.Store(req)
   215  
   216  	shouldBlock := ct.blocking && opts.MinIndex > 0 && opts.MinIndex == index
   217  	if shouldBlock {
   218  		// Wait for return to be triggered. We ignore timeouts based on opts.Timeout
   219  		// since in practice they will always be way longer than our tests run for
   220  		// and the caller can simulate timeout by triggering return without changing
   221  		// index or value.
   222  		ct.triggerMu.Lock()
   223  		ct.trigger.Wait()
   224  		ct.triggerMu.Unlock()
   225  	}
   226  
   227  	// reload index as it probably got bumped
   228  	index = atomic.LoadUint64(&ct.index)
   229  	val := ct.value.Load()
   230  
   231  	if err, ok := val.(error); ok {
   232  		return cache.FetchResult{
   233  			Value: nil,
   234  			Index: index,
   235  		}, err
   236  	}
   237  	return cache.FetchResult{
   238  		Value: val,
   239  		Index: index,
   240  	}, nil
   241  }
   242  
   243  // SupportsBlocking implements cache.Type
   244  func (ct *ControllableCacheType) SupportsBlocking() bool {
   245  	return ct.blocking
   246  }