1/*
2Copyright 2016 Google LLC
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package bigtable
18
19import (
20	"context"
21	"errors"
22	"flag"
23	"fmt"
24	"os"
25	"strings"
26	"time"
27
28	"cloud.google.com/go/bigtable/bttest"
29	btopt "cloud.google.com/go/bigtable/internal/option"
30	"cloud.google.com/go/internal/testutil"
31	"google.golang.org/api/option"
32	gtransport "google.golang.org/api/transport/grpc"
33	"google.golang.org/grpc"
34	"google.golang.org/grpc/peer"
35)
36
37var legacyUseProd string
38var integrationConfig IntegrationTestConfig
39
40var (
41	runCreateInstanceTests bool
42	instanceToCreateZone   string
43	instanceToCreateZone2  string
44	blackholeDpv6Cmd       string
45	blackholeDpv4Cmd       string
46	allowDpv6Cmd           string
47	allowDpv4Cmd           string
48)
49
50func init() {
51	c := &integrationConfig
52
53	flag.BoolVar(&c.UseProd, "it.use-prod", false, "Use remote bigtable instead of local emulator")
54	flag.StringVar(&c.AdminEndpoint, "it.admin-endpoint", "", "Admin api host and port")
55	flag.StringVar(&c.DataEndpoint, "it.data-endpoint", "", "Data api host and port")
56	flag.StringVar(&c.Project, "it.project", "", "Project to use for integration test")
57	flag.StringVar(&c.Instance, "it.instance", "", "Bigtable instance to use")
58	flag.StringVar(&c.Cluster, "it.cluster", "", "Bigtable cluster to use")
59	flag.StringVar(&c.Table, "it.table", "", "Bigtable table to create")
60	flag.BoolVar(&c.AttemptDirectPath, "it.attempt-directpath", false, "Attempt DirectPath")
61	flag.BoolVar(&c.DirectPathIPV4Only, "it.directpath-ipv4-only", false, "Run DirectPath on a ipv4-only VM")
62
63	// Backwards compat
64	flag.StringVar(&legacyUseProd, "use_prod", "", `DEPRECATED: if set to "proj,instance,table", run integration test against production`)
65
66	// Don't test instance creation by default, as quota is necessary and aborted tests could strand resources.
67	flag.BoolVar(&runCreateInstanceTests, "it.run-create-instance-tests", true,
68		"Run tests that create instances as part of executing. Requires sufficient Cloud Bigtable quota. Requires that it.use-prod is true.")
69	flag.StringVar(&instanceToCreateZone, "it.instance-to-create-zone", "us-central1-b",
70		"The zone in which to create the new test instance.")
71	flag.StringVar(&instanceToCreateZone2, "it.instance-to-create-zone2", "us-east1-c",
72		"The zone in which to create a second cluster in the test instance.")
73	// Use sysctl or iptables to blackhole DirectPath IP for fallback tests.
74	flag.StringVar(&blackholeDpv6Cmd, "it.blackhole-dpv6-cmd", "", "Command to make LB and backend addresses blackholed over dpv6")
75	flag.StringVar(&blackholeDpv4Cmd, "it.blackhole-dpv4-cmd", "", "Command to make LB and backend addresses blackholed over dpv4")
76	flag.StringVar(&allowDpv6Cmd, "it.allow-dpv6-cmd", "", "Command to make LB and backend addresses allowed over dpv6")
77	flag.StringVar(&allowDpv4Cmd, "it.allow-dpv4-cmd", "", "Command to make LB and backend addresses allowed over dpv4")
78}
79
80// IntegrationTestConfig contains parameters to pick and setup a IntegrationEnv for testing
81type IntegrationTestConfig struct {
82	UseProd            bool
83	AdminEndpoint      string
84	DataEndpoint       string
85	Project            string
86	Instance           string
87	Cluster            string
88	Table              string
89	AttemptDirectPath  bool
90	DirectPathIPV4Only bool
91}
92
93// IntegrationEnv represents a testing environment.
94// The environment can be implemented using production or an emulator
95type IntegrationEnv interface {
96	Config() IntegrationTestConfig
97	NewAdminClient() (*AdminClient, error)
98	// NewInstanceAdminClient will return nil if instance administration is unsupported in this environment
99	NewInstanceAdminClient() (*InstanceAdminClient, error)
100	NewClient() (*Client, error)
101	Close()
102	Peer() *peer.Peer
103}
104
105// NewIntegrationEnv creates a new environment based on the command line args
106func NewIntegrationEnv() (IntegrationEnv, error) {
107	c := &integrationConfig
108
109	// Check if config settings aren't set. If not, populate from env vars.
110	if c.Project == "" {
111		c.Project = os.Getenv("GCLOUD_TESTS_GOLANG_PROJECT_ID")
112	}
113	if c.Instance == "" {
114		c.Instance = os.Getenv("GCLOUD_TESTS_BIGTABLE_INSTANCE")
115	}
116	if c.Cluster == "" {
117		c.Cluster = os.Getenv("GCLOUD_TESTS_BIGTABLE_CLUSTER")
118	}
119
120	if legacyUseProd != "" {
121		fmt.Println("WARNING: using legacy commandline arg -use_prod, please switch to -it.*")
122		parts := strings.SplitN(legacyUseProd, ",", 3)
123		c.UseProd = true
124		c.Project = parts[0]
125		c.Instance = parts[1]
126		c.Table = parts[2]
127	}
128
129	if c.Instance != "" || c.Cluster != "" {
130		// If commandline args were specified for a live instance, set UseProd
131		c.UseProd = true
132	}
133
134	if integrationConfig.UseProd {
135		if c.Table == "" {
136			c.Table = fmt.Sprintf("it-table-%d", time.Now().Unix())
137		}
138		return NewProdEnv(*c)
139	}
140	return NewEmulatedEnv(*c)
141}
142
143// EmulatedEnv encapsulates the state of an emulator
144type EmulatedEnv struct {
145	config IntegrationTestConfig
146	server *bttest.Server
147}
148
149// NewEmulatedEnv builds and starts the emulator based environment
150func NewEmulatedEnv(config IntegrationTestConfig) (*EmulatedEnv, error) {
151	srv, err := bttest.NewServer("localhost:0", grpc.MaxRecvMsgSize(200<<20), grpc.MaxSendMsgSize(100<<20))
152	if err != nil {
153		return nil, err
154	}
155
156	if config.Project == "" {
157		config.Project = "project"
158	}
159	if config.Instance == "" {
160		config.Instance = "instance"
161	}
162	if config.Table == "" {
163		config.Table = "mytable"
164	}
165	config.AdminEndpoint = srv.Addr
166	config.DataEndpoint = srv.Addr
167
168	env := &EmulatedEnv{
169		config: config,
170		server: srv,
171	}
172	return env, nil
173}
174
175func (e *EmulatedEnv) Peer() *peer.Peer {
176	return nil
177}
178
179// Close stops & cleans up the emulator
180func (e *EmulatedEnv) Close() {
181	e.server.Close()
182}
183
184// Config gets the config used to build this environment
185func (e *EmulatedEnv) Config() IntegrationTestConfig {
186	return e.config
187}
188
189var headersInterceptor = testutil.DefaultHeadersEnforcer()
190
191// NewAdminClient builds a new connected admin client for this environment
192func (e *EmulatedEnv) NewAdminClient() (*AdminClient, error) {
193	o, err := btopt.DefaultClientOptions(e.server.Addr, e.server.Addr, AdminScope, clientUserAgent)
194	if err != nil {
195		return nil, err
196	}
197	// Add gRPC client interceptors to supply Google client information.
198	//
199	// Inject interceptors from headersInterceptor, since they are used to verify
200	// client requests under test.
201	o = append(o, btopt.ClientInterceptorOptions(
202		headersInterceptor.StreamInterceptors(),
203		headersInterceptor.UnaryInterceptors())...)
204
205	timeout := 20 * time.Second
206	ctx, _ := context.WithTimeout(context.Background(), timeout)
207
208	o = append(o, option.WithGRPCDialOption(grpc.WithBlock()))
209	conn, err := gtransport.DialInsecure(ctx, o...)
210	if err != nil {
211		return nil, err
212	}
213
214	return NewAdminClient(ctx, e.config.Project, e.config.Instance,
215		option.WithGRPCConn(conn))
216}
217
218// NewInstanceAdminClient returns nil for the emulated environment since the API is not implemented.
219func (e *EmulatedEnv) NewInstanceAdminClient() (*InstanceAdminClient, error) {
220	return nil, nil
221}
222
223// NewClient builds a new connected data client for this environment
224func (e *EmulatedEnv) NewClient() (*Client, error) {
225	o, err := btopt.DefaultClientOptions(e.server.Addr, e.server.Addr, Scope, clientUserAgent)
226	if err != nil {
227		return nil, err
228	}
229	// Add gRPC client interceptors to supply Google client information.
230	//
231	// Inject interceptors from headersInterceptor, since they are used to verify
232	// client requests under test.
233	o = append(o, btopt.ClientInterceptorOptions(
234		headersInterceptor.StreamInterceptors(),
235		headersInterceptor.UnaryInterceptors())...)
236
237	timeout := 20 * time.Second
238	ctx, _ := context.WithTimeout(context.Background(), timeout)
239
240	o = append(o, option.WithGRPCDialOption(grpc.WithBlock()))
241	o = append(o, option.WithGRPCDialOption(grpc.WithDefaultCallOptions(
242		grpc.MaxCallSendMsgSize(100<<20), grpc.MaxCallRecvMsgSize(100<<20))))
243	conn, err := gtransport.DialInsecure(ctx, o...)
244	if err != nil {
245		return nil, err
246	}
247	return NewClient(ctx, e.config.Project, e.config.Instance, option.WithGRPCConn(conn))
248}
249
250// ProdEnv encapsulates the state necessary to connect to the external Bigtable service
251type ProdEnv struct {
252	config   IntegrationTestConfig
253	peerInfo *peer.Peer
254}
255
256// NewProdEnv builds the environment representation
257func NewProdEnv(config IntegrationTestConfig) (*ProdEnv, error) {
258	if config.Project == "" {
259		return nil, errors.New("Project not set")
260	}
261	if config.Instance == "" {
262		return nil, errors.New("Instance not set")
263	}
264	if config.Cluster == "" {
265		return nil, errors.New("Cluster not set")
266	}
267	if config.Table == "" {
268		return nil, errors.New("Table not set")
269	}
270
271	env := &ProdEnv{
272		config:   config,
273		peerInfo: &peer.Peer{},
274	}
275	return env, nil
276}
277
278func (e *ProdEnv) Peer() *peer.Peer {
279	return e.peerInfo
280}
281
282// Close is a no-op for production environments
283func (e *ProdEnv) Close() {}
284
285// Config gets the config used to build this environment
286func (e *ProdEnv) Config() IntegrationTestConfig {
287	return e.config
288}
289
290// NewAdminClient builds a new connected admin client for this environment
291func (e *ProdEnv) NewAdminClient() (*AdminClient, error) {
292	clientOpts := headersInterceptor.CallOptions()
293	if endpoint := e.config.AdminEndpoint; endpoint != "" {
294		clientOpts = append(clientOpts, option.WithEndpoint(endpoint))
295	}
296	return NewAdminClient(context.Background(), e.config.Project, e.config.Instance, clientOpts...)
297}
298
299// NewInstanceAdminClient returns a new connected instance admin client for this environment
300func (e *ProdEnv) NewInstanceAdminClient() (*InstanceAdminClient, error) {
301	clientOpts := headersInterceptor.CallOptions()
302	if endpoint := e.config.AdminEndpoint; endpoint != "" {
303		clientOpts = append(clientOpts, option.WithEndpoint(endpoint))
304	}
305	return NewInstanceAdminClient(context.Background(), e.config.Project, clientOpts...)
306}
307
308// NewClient builds a connected data client for this environment
309func (e *ProdEnv) NewClient() (*Client, error) {
310	clientOpts := headersInterceptor.CallOptions()
311	if endpoint := e.config.DataEndpoint; endpoint != "" {
312		clientOpts = append(clientOpts, option.WithEndpoint(endpoint))
313	}
314
315	if e.config.AttemptDirectPath {
316		// For DirectPath tests, we need to add an interceptor to check the peer IP.
317		clientOpts = append(clientOpts, option.WithGRPCDialOption(grpc.WithDefaultCallOptions(grpc.Peer(e.peerInfo))))
318	}
319
320	return NewClient(context.Background(), e.config.Project, e.config.Instance, clientOpts...)
321}
322