-
Notifications
You must be signed in to change notification settings - Fork 444
/
test_context.go
359 lines (302 loc) · 13.6 KB
/
test_context.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
package e2e
import (
"context"
"fmt"
"github.com/solo-io/gloo/test/services/envoy"
"github.com/solo-io/gloo/test/testutils"
"github.com/solo-io/solo-kit/pkg/api/v1/resources/core"
"github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
v1 "github.com/solo-io/gloo/projects/gateway/pkg/api/v1"
gatewaydefaults "github.com/solo-io/gloo/projects/gateway/pkg/defaults"
gloov1 "github.com/solo-io/gloo/projects/gloo/pkg/api/v1"
"github.com/solo-io/gloo/projects/gloo/pkg/api/v1/gloosnapshot"
"github.com/solo-io/gloo/projects/gloo/pkg/defaults"
"github.com/solo-io/gloo/test/helpers"
"github.com/solo-io/gloo/test/services"
"github.com/solo-io/gloo/test/v1helpers"
"github.com/solo-io/solo-kit/pkg/api/v1/clients"
"github.com/solo-io/solo-kit/pkg/api/v1/resources"
)
const (
WriteNamespace = defaults.GlooSystem
DefaultVirtualServiceName = "vs-test"
DefaultRouteName = "route-test"
DefaultGatewayName = gatewaydefaults.GatewayProxyName
DefaultProxyName = gatewaydefaults.GatewayProxyName
// DefaultHost defines the Host header that should be used to route traffic to the
// default VirtualService that the TestContext creates
// To make our tests more explicit we define VirtualServices with an explicit set
// of domains (which match the `Host` header of a request), and DefaultHost
// is the domain we use by default
DefaultHost = "test.com"
)
var (
envoyRole = fmt.Sprintf("%v~%v", WriteNamespace, DefaultProxyName)
)
type TestContextFactory struct {
EnvoyFactory envoy.Factory
VaultFactory *services.VaultFactory
ConsulFactory *services.ConsulFactory
}
func (f *TestContextFactory) NewTestContext(testRequirements ...testutils.Requirement) *TestContext {
// Skip or Fail tests which do not satisfy the provided requirements
testutils.ValidateRequirementsAndNotifyGinkgo(testRequirements...)
return &TestContext{
envoyInstance: f.EnvoyFactory.NewInstance(),
testUpstreamGenerator: v1helpers.NewTestHttpUpstream,
}
}
func (f *TestContextFactory) NewTestContextWithVault(testRequirements ...testutils.Requirement) *TestContextWithVault {
requirementsWithVault := append(testRequirements, testutils.Vault())
testContext := f.NewTestContext(requirementsWithVault...)
return &TestContextWithVault{
TestContext: testContext,
vaultInstance: f.VaultFactory.MustVaultInstance(),
}
}
func (f *TestContextFactory) NewTestContextWithConsul(testRequirements ...testutils.Requirement) *TestContextWithConsul {
requirementsWithConsul := append(testRequirements, testutils.Consul())
testContext := f.NewTestContext(requirementsWithConsul...)
return &TestContextWithConsul{
TestContext: testContext,
consulInstance: f.ConsulFactory.MustConsulInstance(),
}
}
// TestContext represents the aggregate set of configuration needed to run a single e2e test
// It is intended to remove some boilerplate setup/teardown of tests out of the test themselves
// to ensure that tests are easier to read and maintain since they only contain the resource changes
// that we are validating
type TestContext struct {
ctx context.Context
cancel context.CancelFunc
envoyInstance *envoy.Instance
runOptions *services.RunOptions
testClients services.TestClients
testUpstream *v1helpers.TestUpstream
testUpstreamGenerator func(ctx context.Context, addr string) *v1helpers.TestUpstream
resourcesToCreate *gloosnapshot.ApiSnapshot
}
func (c *TestContext) BeforeEach() {
ginkgo.By("TestContext.BeforeEach: Setting up default configuration")
c.ctx, c.cancel = context.WithCancel(context.Background())
c.testUpstream = c.testUpstreamGenerator(c.ctx, c.EnvoyInstance().LocalAddr())
c.runOptions = &services.RunOptions{
NsToWrite: WriteNamespace,
NsToWatch: []string{"default", WriteNamespace},
WhatToRun: services.What{
DisableGateway: false,
DisableFds: true,
DisableUds: true,
},
}
vsToTestUpstream := helpers.NewVirtualServiceBuilder().
WithName(DefaultVirtualServiceName).
WithNamespace(WriteNamespace).
WithDomain(DefaultHost).
WithRoutePrefixMatcher(DefaultRouteName, "/").
WithRouteActionToUpstream(DefaultRouteName, c.testUpstream.Upstream).
Build()
// The set of resources that these tests will generate
// Individual tests may modify these resources, but we provide the default resources
// required to form a Proxy and handle requests
c.resourcesToCreate = &gloosnapshot.ApiSnapshot{
Gateways: v1.GatewayList{
gatewaydefaults.DefaultGateway(WriteNamespace),
},
VirtualServices: v1.VirtualServiceList{
vsToTestUpstream,
},
Upstreams: gloov1.UpstreamList{
c.testUpstream.Upstream,
},
}
}
func (c *TestContext) AfterEach() {
ginkgo.By("TestContext.AfterEach: Cancelling test context")
// All services connected to the TestContext are tied to the context, so cancelling it will clean those up
c.cancel()
}
func (c *TestContext) JustBeforeEach() {
ginkgo.By("TestContext.JustBeforeEach: Running Gloo and Envoy, writing resource snapshot to storage")
// Run Gloo
c.testClients = services.RunGlooGatewayUdsFds(c.ctx, c.runOptions)
// Run Envoy
err := c.envoyInstance.RunWith(envoy.RunConfig{
Context: c.ctx,
Role: envoyRole,
Port: uint32(c.testClients.GlooPort),
RestXdsPort: uint32(c.testClients.RestXdsPort),
})
ExpectWithOffset(1, err).NotTo(HaveOccurred())
// Create Resources
err = c.testClients.WriteSnapshot(c.ctx, c.resourcesToCreate)
ExpectWithOffset(1, err).NotTo(HaveOccurred())
// Wait for a proxy to be accepted
c.EventuallyProxyAccepted()
}
func (c *TestContext) JustAfterEach() {
// We do not need to clean up the Snapshot that was written in the JustBeforeEach
// That is because each test uses its own InMemoryCache
}
// SetRunSettings can be used to modify the runtime Settings object for a test
// This should be called after the TestContext.BeforeEach (when the default settings are applied)
// and before the TestContext.JustBeforeEach (when the settings are consumed)
func (c *TestContext) SetRunSettings(settings *gloov1.Settings) {
c.runOptions.Settings = settings
}
// SetRunServices can be used to modify the services (gloo, fds, uds) which will run for a test
// This should be called after the TestContext.BeforeEach (when the default services are applied)
// and before the TestContext.JustBeforeEach (when the services are run)
func (c *TestContext) SetRunServices(services services.What) {
c.runOptions.WhatToRun = services
}
// Ctx returns the Context maintained by the TestContext
// The Context is cancelled during the AfterEach portion of tests
func (c *TestContext) Ctx() context.Context {
return c.ctx
}
// ResourcesToCreate returns the ApiSnapshot of resources the TestContext maintains
// This snapshot is what is written to storage during the JustBeforeEach portion
// We return a reference to the object, so that individual tests can modify the snapshot
// before we write it to storage
func (c *TestContext) ResourcesToCreate() *gloosnapshot.ApiSnapshot {
return c.resourcesToCreate
}
// EnvoyInstance returns the wrapper for the running instance of Envoy that this test is using
// It contains utility methods to easily inspect the live configuration and statistics for the instance
func (c *TestContext) EnvoyInstance() *envoy.Instance {
return c.envoyInstance
}
// TestUpstream returns the TestUpstream object that the TestContext built
// A TestUpstream is used to run an echo server and define the Gloo Upstream object to route to it
func (c *TestContext) TestUpstream() *v1helpers.TestUpstream {
return c.testUpstream
}
// TestClients returns the set of resource clients that can be used to perform CRUD operations
// on resources used by these tests
// Instead of using the resource clients directly, we recommend placing resources on the
// ResourcesToCreate object, and letting the TestContext handle the lifecycle of those objects
func (c *TestContext) TestClients() services.TestClients {
return c.testClients
}
// ReadDefaultProxy returns the Proxy object that will be generated by the resources in the TestContext
func (c *TestContext) ReadDefaultProxy() (*gloov1.Proxy, error) {
return c.testClients.ProxyClient.Read(WriteNamespace, DefaultProxyName, clients.ReadOpts{Ctx: c.ctx})
}
// PatchDefaultVirtualService mutates the existing VirtualService generated by the TestContext
func (c *TestContext) PatchDefaultVirtualService(mutator func(*v1.VirtualService) *v1.VirtualService) {
err := helpers.PatchResourceWithOffset(
1,
c.ctx,
&core.ResourceRef{
Name: DefaultVirtualServiceName,
Namespace: WriteNamespace,
},
func(resource resources.Resource) resources.Resource {
return mutator(resource.(*v1.VirtualService))
},
c.testClients.VirtualServiceClient.BaseClient(),
)
ExpectWithOffset(1, err).NotTo(HaveOccurred())
}
// PatchDefaultGateway mutates the existing Gateway generated by the TestContext
func (c *TestContext) PatchDefaultGateway(mutator func(*v1.Gateway) *v1.Gateway) {
err := helpers.PatchResourceWithOffset(
1,
c.ctx,
&core.ResourceRef{
Name: DefaultGatewayName,
Namespace: WriteNamespace,
},
func(resource resources.Resource) resources.Resource {
return mutator(resource.(*v1.Gateway))
},
c.testClients.GatewayClient.BaseClient(),
)
ExpectWithOffset(1, err).NotTo(HaveOccurred())
}
// PatchDefaultUpstream mutates the existing Upstream generated by the TestContext
func (c *TestContext) PatchDefaultUpstream(mutator func(*gloov1.Upstream) *gloov1.Upstream) {
usRef := c.testUpstream.Upstream.GetMetadata().Ref()
err := helpers.PatchResourceWithOffset(
1,
c.ctx,
usRef,
func(resource resources.Resource) resources.Resource {
return mutator(resource.(*gloov1.Upstream))
},
c.testClients.UpstreamClient.BaseClient(),
)
ExpectWithOffset(1, err).NotTo(HaveOccurred())
}
// SetUpstreamGenerator Use a different function to create a test upstream call before testContext.BeforeEach()
// Used for example with helpers.NewTestGrpcUpstream which has the side effect of also starting a grpc service
func (c *TestContext) SetUpstreamGenerator(generator func(ctx context.Context, addr string) *v1helpers.TestUpstream) {
c.testUpstreamGenerator = generator
}
// For tests that rely on changing an existing configuration.
func (c *TestContext) EventuallyProxyAccepted() {
// Wait for a proxy to be accepted
helpers.EventuallyResourceAccepted(func() (resources.InputResource, error) {
return c.testClients.ProxyClient.Read(WriteNamespace, DefaultProxyName, clients.ReadOpts{Ctx: c.ctx})
})
}
// GetHttpRequestBuilder returns an HttpRequestBuilder to easily build http requests used in e2e tests
func (c *TestContext) GetHttpRequestBuilder() *testutils.HttpRequestBuilder {
return testutils.DefaultRequestBuilder().
WithScheme("http").
WithHostname("localhost").
WithContentType("application/octet-stream").
WithPort(c.envoyInstance.HttpPort). // When running Envoy locally, we port-forward this port to accept http traffic locally
WithHost(DefaultHost) // The default Virtual Service routes traffic only with a particular Host header
}
// GetHttpsRequestBuilder returns an HttpRequestBuilder to easily build https requests used in e2e tests
func (c *TestContext) GetHttpsRequestBuilder() *testutils.HttpRequestBuilder {
return testutils.DefaultRequestBuilder().
WithScheme("https").
WithHostname("localhost").
WithContentType("application/octet-stream").
WithPort(c.envoyInstance.HttpsPort). // When running Envoy locally, we port-forward this port to accept https traffic locally
WithHost(DefaultHost) // The default Virtual Service routes traffic only with a particular Host header
}
// TestContextWithVault represents the aggregate set of configuration needed to run a single e2e test
// using Vault as a secret store. This is used rarely in tests, so we intentionally try to separate the
// vault logic from the core TestContext to avoid adding complexity
type TestContextWithVault struct {
*TestContext
vaultInstance *services.VaultInstance
}
// VaultInstance returns the wrapper for the running instance of Vault that this test is using
func (v *TestContextWithVault) VaultInstance() *services.VaultInstance {
return v.vaultInstance
}
// RunVault starts running the VaultInstance and blocks until it has successfully started
func (v *TestContextWithVault) RunVault() {
ginkgo.By("TestContextWithVault: Running Vault")
// The VaultInstance will be cleaned up when the provided context is cancelled
// By running Vault with the TestContext.Ctxt, we can be sure that when the TestContext
// completes, Vault will be cleaned up
err := v.VaultInstance().Run(v.Ctx())
ExpectWithOffset(1, err).NotTo(HaveOccurred())
}
// TestContextWithConsul represents the aggregate set of configuration needed to run a single e2e test
// using Consul as a service registry to route traffic to. This is used rarely in tests,
// so we intentionally try to separate the consul logic from the core TestContext to avoid adding complexity
type TestContextWithConsul struct {
*TestContext
consulInstance *services.ConsulInstance
}
// ConsulInstance returns the wrapper for the running instance of Vault that this test is using
func (c *TestContextWithConsul) ConsulInstance() *services.ConsulInstance {
return c.consulInstance
}
// RunConsul starts running the ConsulInstance and blocks until it has successfully started
func (c *TestContextWithConsul) RunConsul() {
ginkgo.By("TestContextWithConsul: Running Consul")
// The ConsulInstance will be cleaned up when the provided context is cancelled
// By running Consul with the TestContext.Ctxt, we can be sure that when the TestContext
// completes, Consul will be cleaned up
err := c.ConsulInstance().Run(c.Ctx())
ExpectWithOffset(1, err).NotTo(HaveOccurred())
}