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