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	"errors"
21	"flag"
22	"fmt"
23	"strings"
24	"time"
25
26	"cloud.google.com/go/bigtable/bttest"
27	"golang.org/x/net/context"
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	} else {
89		return NewEmulatedEnv(c)
90	}
91}
92
93// EmulatedEnv encapsulates the state of an emulator
94type EmulatedEnv struct {
95	config IntegrationTestConfig
96	server *bttest.Server
97}
98
99// NewEmulatedEnv builds and starts the emulator based environment
100func NewEmulatedEnv(config IntegrationTestConfig) (*EmulatedEnv, error) {
101	srv, err := bttest.NewServer("localhost:0", grpc.MaxRecvMsgSize(200<<20), grpc.MaxSendMsgSize(100<<20))
102	if err != nil {
103		return nil, err
104	}
105
106	if config.Project == "" {
107		config.Project = "project"
108	}
109	if config.Instance == "" {
110		config.Instance = "instance"
111	}
112	if config.Table == "" {
113		config.Table = "mytable"
114	}
115	config.AdminEndpoint = srv.Addr
116	config.DataEndpoint = srv.Addr
117
118	env := &EmulatedEnv{
119		config: config,
120		server: srv,
121	}
122	return env, nil
123}
124
125// Close stops & cleans up the emulator
126func (e *EmulatedEnv) Close() {
127	e.server.Close()
128}
129
130// Config gets the config used to build this environment
131func (e *EmulatedEnv) Config() IntegrationTestConfig {
132	return e.config
133}
134
135// NewAdminClient builds a new connected admin client for this environment
136func (e *EmulatedEnv) NewAdminClient() (*AdminClient, error) {
137	timeout := 20 * time.Second
138	ctx, _ := context.WithTimeout(context.Background(), timeout)
139	conn, err := grpc.Dial(e.server.Addr, grpc.WithInsecure(), grpc.WithBlock())
140	if err != nil {
141		return nil, err
142	}
143	return NewAdminClient(ctx, e.config.Project, e.config.Instance, option.WithGRPCConn(conn))
144}
145
146// NewInstanceAdminClient returns nil for the emulated environment since the API is not implemented.
147func (e *EmulatedEnv) NewInstanceAdminClient() (*InstanceAdminClient, error) {
148	return nil, nil
149}
150
151// NewClient builds a new connected data client for this environment
152func (e *EmulatedEnv) NewClient() (*Client, error) {
153	timeout := 20 * time.Second
154	ctx, _ := context.WithTimeout(context.Background(), timeout)
155	conn, err := grpc.Dial(e.server.Addr, grpc.WithInsecure(), grpc.WithBlock(),
156		grpc.WithDefaultCallOptions(grpc.MaxCallSendMsgSize(100<<20), grpc.MaxCallRecvMsgSize(100<<20)))
157	if err != nil {
158		return nil, err
159	}
160	return NewClient(ctx, e.config.Project, e.config.Instance, option.WithGRPCConn(conn))
161}
162
163// ProdEnv encapsulates the state necessary to connect to the external Bigtable service
164type ProdEnv struct {
165	config IntegrationTestConfig
166}
167
168// NewProdEnv builds the environment representation
169func NewProdEnv(config IntegrationTestConfig) (*ProdEnv, error) {
170	if config.Project == "" {
171		return nil, errors.New("Project not set")
172	}
173	if config.Instance == "" {
174		return nil, errors.New("Instance not set")
175	}
176	if config.Table == "" {
177		return nil, errors.New("Table not set")
178	}
179
180	return &ProdEnv{config}, nil
181}
182
183// Close is a no-op for production environments
184func (e *ProdEnv) Close() {}
185
186// Config gets the config used to build this environment
187func (e *ProdEnv) Config() IntegrationTestConfig {
188	return e.config
189}
190
191// NewAdminClient builds a new connected admin client for this environment
192func (e *ProdEnv) NewAdminClient() (*AdminClient, error) {
193	var clientOpts []option.ClientOption
194	if endpoint := e.config.AdminEndpoint; endpoint != "" {
195		clientOpts = append(clientOpts, option.WithEndpoint(endpoint))
196	}
197	return NewAdminClient(context.Background(), e.config.Project, e.config.Instance, clientOpts...)
198}
199
200// NewInstanceAdminClient returns a new connected instance admin client for this environment
201func (e *ProdEnv) NewInstanceAdminClient() (*InstanceAdminClient, error) {
202	var clientOpts []option.ClientOption
203	if endpoint := e.config.AdminEndpoint; endpoint != "" {
204		clientOpts = append(clientOpts, option.WithEndpoint(endpoint))
205	}
206	return NewInstanceAdminClient(context.Background(), e.config.Project, clientOpts...)
207}
208
209// NewClient builds a connected data client for this environment
210func (e *ProdEnv) NewClient() (*Client, error) {
211	var clientOpts []option.ClientOption
212	if endpoint := e.config.DataEndpoint; endpoint != "" {
213		clientOpts = append(clientOpts, option.WithEndpoint(endpoint))
214	}
215	return NewClient(context.Background(), e.config.Project, e.config.Instance, clientOpts...)
216}
217