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