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	"strings"
25	"time"
26
27	"cloud.google.com/go/bigtable/bttest"
28	btopt "cloud.google.com/go/bigtable/internal/option"
29	"cloud.google.com/go/internal/testutil"
30	"google.golang.org/api/option"
31	gtransport "google.golang.org/api/transport/grpc"
32	"google.golang.org/grpc"
33)
34
35var legacyUseProd string
36var integrationConfig IntegrationTestConfig
37
38func init() {
39	c := &integrationConfig
40
41	flag.BoolVar(&c.UseProd, "it.use-prod", false, "Use remote bigtable instead of local emulator")
42	flag.StringVar(&c.AdminEndpoint, "it.admin-endpoint", "", "Admin api host and port")
43	flag.StringVar(&c.DataEndpoint, "it.data-endpoint", "", "Data api host and port")
44	flag.StringVar(&c.Project, "it.project", "", "Project to use for integration test")
45	flag.StringVar(&c.Instance, "it.instance", "", "Bigtable instance to use")
46	flag.StringVar(&c.Cluster, "it.cluster", "", "Bigtable cluster to use")
47	flag.StringVar(&c.Table, "it.table", "", "Bigtable table to create")
48
49	// Backwards compat
50	flag.StringVar(&legacyUseProd, "use_prod", "", `DEPRECATED: if set to "proj,instance,table", run integration test against production`)
51
52}
53
54// IntegrationTestConfig contains parameters to pick and setup a IntegrationEnv for testing
55type IntegrationTestConfig struct {
56	UseProd       bool
57	AdminEndpoint string
58	DataEndpoint  string
59	Project       string
60	Instance      string
61	Cluster       string
62	Table         string
63}
64
65// IntegrationEnv represents a testing environment.
66// The environment can be implemented using production or an emulator
67type IntegrationEnv interface {
68	Config() IntegrationTestConfig
69	NewAdminClient() (*AdminClient, error)
70	// NewInstanceAdminClient will return nil if instance administration is unsupported in this environment
71	NewInstanceAdminClient() (*InstanceAdminClient, error)
72	NewClient() (*Client, error)
73	Close()
74}
75
76// NewIntegrationEnv creates a new environment based on the command line args
77func NewIntegrationEnv() (IntegrationEnv, error) {
78	c := integrationConfig
79
80	if legacyUseProd != "" {
81		fmt.Println("WARNING: using legacy commandline arg -use_prod, please switch to -it.*")
82		parts := strings.SplitN(legacyUseProd, ",", 3)
83		c.UseProd = true
84		c.Project = parts[0]
85		c.Instance = parts[1]
86		c.Table = parts[2]
87	}
88
89	if integrationConfig.UseProd {
90		return NewProdEnv(c)
91	}
92	return NewEmulatedEnv(c)
93}
94
95// EmulatedEnv encapsulates the state of an emulator
96type EmulatedEnv struct {
97	config IntegrationTestConfig
98	server *bttest.Server
99}
100
101// NewEmulatedEnv builds and starts the emulator based environment
102func NewEmulatedEnv(config IntegrationTestConfig) (*EmulatedEnv, error) {
103	srv, err := bttest.NewServer("localhost:0", grpc.MaxRecvMsgSize(200<<20), grpc.MaxSendMsgSize(100<<20))
104	if err != nil {
105		return nil, err
106	}
107
108	if config.Project == "" {
109		config.Project = "project"
110	}
111	if config.Instance == "" {
112		config.Instance = "instance"
113	}
114	if config.Table == "" {
115		config.Table = "mytable"
116	}
117	config.AdminEndpoint = srv.Addr
118	config.DataEndpoint = srv.Addr
119
120	env := &EmulatedEnv{
121		config: config,
122		server: srv,
123	}
124	return env, nil
125}
126
127// Close stops & cleans up the emulator
128func (e *EmulatedEnv) Close() {
129	e.server.Close()
130}
131
132// Config gets the config used to build this environment
133func (e *EmulatedEnv) Config() IntegrationTestConfig {
134	return e.config
135}
136
137var headersInterceptor = testutil.DefaultHeadersEnforcer()
138
139// NewAdminClient builds a new connected admin client for this environment
140func (e *EmulatedEnv) NewAdminClient() (*AdminClient, error) {
141	o, err := btopt.DefaultClientOptions(e.server.Addr, AdminScope, clientUserAgent)
142	if err != nil {
143		return nil, err
144	}
145	// Add gRPC client interceptors to supply Google client information.
146	//
147	// Inject interceptors from headersInterceptor, since they are used to verify
148	// client requests under test.
149	o = append(o, btopt.ClientInterceptorOptions(
150		headersInterceptor.StreamInterceptors(),
151		headersInterceptor.UnaryInterceptors())...)
152
153	timeout := 20 * time.Second
154	ctx, _ := context.WithTimeout(context.Background(), timeout)
155
156	o = append(o, option.WithGRPCDialOption(grpc.WithBlock()))
157	conn, err := gtransport.DialInsecure(ctx, o...)
158	if err != nil {
159		return nil, err
160	}
161
162	return NewAdminClient(ctx, e.config.Project, e.config.Instance,
163		option.WithGRPCConn(conn))
164}
165
166// NewInstanceAdminClient returns nil for the emulated environment since the API is not implemented.
167func (e *EmulatedEnv) NewInstanceAdminClient() (*InstanceAdminClient, error) {
168	return nil, nil
169}
170
171// NewClient builds a new connected data client for this environment
172func (e *EmulatedEnv) NewClient() (*Client, error) {
173	o, err := btopt.DefaultClientOptions(e.server.Addr, Scope, clientUserAgent)
174	if err != nil {
175		return nil, err
176	}
177	// Add gRPC client interceptors to supply Google client information.
178	//
179	// Inject interceptors from headersInterceptor, since they are used to verify
180	// client requests under test.
181	o = append(o, btopt.ClientInterceptorOptions(
182		headersInterceptor.StreamInterceptors(),
183		headersInterceptor.UnaryInterceptors())...)
184
185	timeout := 20 * time.Second
186	ctx, _ := context.WithTimeout(context.Background(), timeout)
187
188	o = append(o, option.WithGRPCDialOption(grpc.WithBlock()))
189	o = append(o, option.WithGRPCDialOption(grpc.WithDefaultCallOptions(
190		grpc.MaxCallSendMsgSize(100<<20), grpc.MaxCallRecvMsgSize(100<<20))))
191	conn, err := gtransport.DialInsecure(ctx, o...)
192	if err != nil {
193		return nil, err
194	}
195	return NewClient(ctx, e.config.Project, e.config.Instance, option.WithGRPCConn(conn))
196}
197
198// ProdEnv encapsulates the state necessary to connect to the external Bigtable service
199type ProdEnv struct {
200	config IntegrationTestConfig
201}
202
203// NewProdEnv builds the environment representation
204func NewProdEnv(config IntegrationTestConfig) (*ProdEnv, error) {
205	if config.Project == "" {
206		return nil, errors.New("Project not set")
207	}
208	if config.Instance == "" {
209		return nil, errors.New("Instance not set")
210	}
211	if config.Table == "" {
212		return nil, errors.New("Table not set")
213	}
214
215	return &ProdEnv{config}, nil
216}
217
218// Close is a no-op for production environments
219func (e *ProdEnv) Close() {}
220
221// Config gets the config used to build this environment
222func (e *ProdEnv) Config() IntegrationTestConfig {
223	return e.config
224}
225
226// NewAdminClient builds a new connected admin client for this environment
227func (e *ProdEnv) NewAdminClient() (*AdminClient, error) {
228	clientOpts := headersInterceptor.CallOptions()
229	if endpoint := e.config.AdminEndpoint; endpoint != "" {
230		clientOpts = append(clientOpts, option.WithEndpoint(endpoint))
231	}
232	return NewAdminClient(context.Background(), e.config.Project, e.config.Instance, clientOpts...)
233}
234
235// NewInstanceAdminClient returns a new connected instance admin client for this environment
236func (e *ProdEnv) NewInstanceAdminClient() (*InstanceAdminClient, error) {
237	clientOpts := headersInterceptor.CallOptions()
238	if endpoint := e.config.AdminEndpoint; endpoint != "" {
239		clientOpts = append(clientOpts, option.WithEndpoint(endpoint))
240	}
241	return NewInstanceAdminClient(context.Background(), e.config.Project, clientOpts...)
242}
243
244// NewClient builds a connected data client for this environment
245func (e *ProdEnv) NewClient() (*Client, error) {
246	clientOpts := headersInterceptor.CallOptions()
247	if endpoint := e.config.DataEndpoint; endpoint != "" {
248		clientOpts = append(clientOpts, option.WithEndpoint(endpoint))
249	}
250	return NewClient(context.Background(), e.config.Project, e.config.Instance, clientOpts...)
251}
252