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