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 }