github.com/google/cloudprober@v0.11.3/rds/gcp/gce_instances_test.go (about) 1 // Copyright 2017-2020 The Cloudprober Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package gcp 16 17 import ( 18 "fmt" 19 "io/ioutil" 20 "net" 21 "net/http" 22 "reflect" 23 "sort" 24 "testing" 25 "time" 26 27 "github.com/golang/protobuf/proto" 28 "github.com/google/cloudprober/logger" 29 pb "github.com/google/cloudprober/rds/proto" 30 ) 31 32 type testNetIf struct { 33 privateIP string 34 publicIP string 35 aliasIPRange string 36 ipv6 string 37 publicIPv6 string 38 } 39 40 type testInstance struct { 41 name string 42 zone string 43 netIf []*testNetIf 44 labels map[string]string 45 } 46 47 func (ti *testInstance) data() *instanceData { 48 var cNetIfs []networkInterface 49 for _, ni := range ti.netIf { 50 cNetIf := networkInterface{ 51 NetworkIP: ni.privateIP, 52 Ipv6Address: ni.ipv6, 53 } 54 if ni.publicIP != "" { 55 cNetIf.AccessConfigs = []accessConfig{ 56 {NatIP: ni.publicIP}, 57 } 58 } 59 if ni.publicIPv6 != "" { 60 cNetIf.Ipv6AccessConfigs = []accessConfig{ 61 {ExternalIpv6: ni.publicIPv6}, 62 } 63 } 64 if ni.aliasIPRange != "" { 65 cNetIf.AliasIPRanges = []struct { 66 IPCidrRange string `json:"ipCidrRange,omitempty"` 67 }{ 68 { 69 IPCidrRange: ni.aliasIPRange, 70 }, 71 } 72 } 73 cNetIfs = append(cNetIfs, cNetIf) 74 } 75 76 return &instanceData{ii: &instanceInfo{Name: ti.name, NetworkInterfaces: cNetIfs, Labels: ti.labels}} 77 } 78 79 func (ti *testInstance) expectedIP(spec *testSpec) string { 80 switch spec.ipType { 81 case "private": 82 if spec.ipv6 { 83 return ti.netIf[spec.index].ipv6 84 } 85 return ti.netIf[spec.index].privateIP 86 case "public": 87 if spec.ipv6 { 88 return ti.netIf[spec.index].publicIPv6 89 } 90 return ti.netIf[spec.index].publicIP 91 case "ipAliasRange": 92 netIP, _, _ := net.ParseCIDR(ti.netIf[spec.index].aliasIPRange) 93 if netIP == nil { 94 netIP = net.ParseIP(ti.netIf[spec.index].aliasIPRange) 95 } 96 return netIP.String() 97 } 98 return "" 99 } 100 101 var testInstancesData = []*testInstance{ 102 { 103 name: "ins1", 104 zone: "z-1", 105 netIf: []*testNetIf{ 106 &testNetIf{"10.216.0.1", "104.100.143.1", "192.168.1.0/24", "2600:2d00:4030:a47:c0a8:2110:0:0", "2600:2d00:4030:a47:c0a8:2110:1:0"}, 107 &testNetIf{"10.216.1.1", "", "", "", ""}, 108 }, 109 labels: map[string]string{ 110 "env": "staging", 111 "func": "rds", 112 }, 113 }, 114 { 115 name: "ins2", 116 zone: "z-2", 117 netIf: []*testNetIf{ 118 &testNetIf{"10.216.0.2", "104.100.143.2", "192.168.2.0", "", ""}, 119 &testNetIf{"10.216.1.2", "104.100.143.3", "", "", ""}, 120 }, 121 labels: map[string]string{ 122 "env": "prod", 123 "func": "rds", 124 }, 125 }, 126 { 127 name: "ins3", 128 zone: "z-2", 129 netIf: []*testNetIf{ 130 &testNetIf{"10.216.0.3", "104.100.143.4", "192.168.3.0", "", ""}, 131 &testNetIf{"10.216.1.3", "104.100.143.5", "", "", ""}, 132 }, 133 labels: map[string]string{ 134 "env": "prod", 135 "func": "rds", 136 }, 137 }, 138 } 139 140 func testLister() *gceInstancesLister { 141 // Initialize instanceLister manually for testing, using the 142 // testInstances data. Default initialization invokes GCE APIs which we want 143 // to avoid. 144 lister := &gceInstancesLister{ 145 cachePerScope: make(map[string]map[string]*instanceData), 146 namesPerScope: make(map[string][]string), 147 l: &logger.Logger{}, 148 } 149 150 for _, ti := range testInstancesData { 151 if lister.cachePerScope[ti.zone] == nil { 152 lister.cachePerScope[ti.zone] = make(map[string]*instanceData) 153 } 154 lister.cachePerScope[ti.zone][ti.name] = ti.data() 155 lister.namesPerScope[ti.zone] = append(lister.namesPerScope[ti.zone], ti.name) 156 } 157 158 return lister 159 } 160 161 type testSpec struct { 162 f []*pb.Filter 163 index int 164 ipType string 165 ipv6 bool 166 err bool 167 } 168 169 func (ts *testSpec) ipConfig() *pb.IPConfig { 170 ipConfig := &pb.IPConfig{} 171 if ts.ipv6 { 172 ipConfig.IpVersion = pb.IPConfig_IPV6.Enum() 173 } 174 switch ts.ipType { 175 case "public": 176 ipConfig.IpType = pb.IPConfig_PUBLIC.Enum() 177 case "ipAliasRange": 178 ipConfig.IpType = pb.IPConfig_ALIAS.Enum() 179 } 180 if ts.index != 0 { 181 ipConfig.NicIndex = proto.Int32(int32(ts.index)) 182 } 183 return ipConfig 184 } 185 186 func testListResources(t *testing.T, gil *gceInstancesLister, expectedInstances []*testInstance, spec *testSpec) { 187 t.Helper() 188 189 if spec == nil { 190 spec = &testSpec{} 191 } 192 if spec.ipType == "" { 193 spec.ipType = "private" 194 } 195 196 var filters []*pb.Filter 197 if spec.f != nil { 198 filters = append(filters, spec.f...) 199 } 200 201 resources, err := gil.listResources(&pb.ListResourcesRequest{ 202 Filter: filters, 203 IpConfig: spec.ipConfig(), 204 }) 205 206 if err != nil { 207 if !spec.err { 208 t.Errorf("Got error while listing resources: %v, expected no errors", err) 209 } 210 return 211 } 212 213 if len(resources) != len(expectedInstances) { 214 t.Errorf("Got wrong number of targets. Expected: %d, Got: %d", len(expectedInstances), len(resources)) 215 } 216 217 // Build a map from the resources in the reply. 218 instances := make(map[string]string) 219 for _, res := range resources { 220 instances[res.GetName()] = res.GetIp() 221 } 222 223 for _, ti := range expectedInstances { 224 ip := instances[ti.name] 225 expectedIP := ti.expectedIP(spec) 226 if ip != expectedIP { 227 t.Errorf("Got wrong <%s> IP for %s. Expected: %s, Got: %s", spec.ipType, ti.name, expectedIP, ip) 228 } 229 } 230 } 231 232 func TestInstancesResourcesFilters(t *testing.T) { 233 lister := testLister() 234 235 // ################################################################# 236 // Instances, with first NIC and private IP. Expect no errors. 237 // ################################################################# 238 testListResources(t, lister, testInstancesData, nil) 239 240 // ################################################################# 241 // Instances, with first NIC and private IP, but a bad filter. 242 // ################################################################# 243 f := []*pb.Filter{ 244 { 245 Key: proto.String("instance_name"), 246 Value: proto.String("ins2"), 247 }, 248 } 249 testListResources(t, lister, testInstancesData[1:], &testSpec{f: f, err: true}) 250 251 // ################################################################# 252 // Instances, matching the name "ins." and labels: env:prod, func:rds 253 // Expect: all instances, no errors. 254 // ################################################################# 255 f = []*pb.Filter{ 256 { 257 Key: proto.String("name"), 258 Value: proto.String("ins."), 259 }, 260 { 261 Key: proto.String("labels.func"), 262 Value: proto.String("rds"), 263 }, 264 } 265 testListResources(t, lister, testInstancesData, &testSpec{f: f}) 266 267 // ################################################################# 268 // Instances, matching the name "ins." and labels: env:prod, func:rds 269 // Expect: Only second instance, no errors. 270 // ################################################################# 271 f = []*pb.Filter{ 272 { 273 Key: proto.String("name"), 274 Value: proto.String("ins."), 275 }, 276 { 277 Key: proto.String("labels.env"), 278 Value: proto.String("prod"), 279 }, 280 { 281 Key: proto.String("labels.func"), 282 Value: proto.String("rds"), 283 }, 284 } 285 testListResources(t, lister, testInstancesData[1:], &testSpec{f: f}) 286 287 // ################################################################# 288 // Instances, matching the name "ins." and labels: env:prod, func:server 289 // Expect: no instance, no errors. 290 // ################################################################# 291 f = []*pb.Filter{ 292 { 293 Key: proto.String("name"), 294 Value: proto.String("ins."), 295 }, 296 { 297 Key: proto.String("labels.env"), 298 Value: proto.String("prod"), 299 }, 300 { 301 Key: proto.String("labels.func"), 302 Value: proto.String("server"), 303 }, 304 } 305 testListResources(t, lister, []*testInstance{}, &testSpec{f: f}) 306 } 307 308 func TestInstancesResources(t *testing.T) { 309 lister := testLister() 310 311 // ################################################################# 312 // Instances, with first NIC and private IP. Expect no errors. 313 // ################################################################# 314 testListResources(t, lister, testInstancesData, nil) 315 316 // ################################################################# 317 // Instances, with first NIC and IPv6 318 // Expect no errors. 319 // ################################################################# 320 testListResources(t, lister, testInstancesData, &testSpec{ipv6: true}) 321 322 // ################################################################# 323 // Instances, with first NIC and public IP 324 // Expect no errors. 325 // ################################################################# 326 testListResources(t, lister, testInstancesData, &testSpec{ipType: "public"}) 327 328 // ################################################################# 329 // Instances (only ins1), with first NIC, IPv6 and public IP. 330 // Expect no errors. 331 // ################################################################# 332 f := []*pb.Filter{ 333 { 334 Key: proto.String("name"), 335 Value: proto.String("ins1"), 336 }, 337 } 338 testListResources(t, lister, testInstancesData[:1], &testSpec{f: f, ipType: "public", ipv6: true}) 339 340 // ################################################################# 341 // Instances, with first NIC and alias IP 342 // Expect no errors. 343 // ################################################################# 344 testListResources(t, lister, testInstancesData, &testSpec{ipType: "ipAliasRange"}) 345 346 // ################################################################# 347 // Instances, with second NIC and private IP 348 // Expect no errors. 349 // ################################################################# 350 testListResources(t, lister, testInstancesData, &testSpec{index: 1}) 351 352 // ################################################################# 353 // Instances, with second NIC and public IP 354 // We get an error as first instance's second NIC doesn't have public IP. 355 // ################################################################## 356 testListResources(t, lister, nil, &testSpec{index: 1, ipType: "public", err: true}) 357 } 358 359 func readTestJSON(t *testing.T, fileName string) (b []byte) { 360 t.Helper() 361 362 instancesListFile := "./testdata/" + fileName 363 data, err := ioutil.ReadFile(instancesListFile) 364 365 if err != nil { 366 t.Fatalf("error reading test data file: %s", instancesListFile) 367 } 368 369 return data 370 } 371 372 func TestExpand(t *testing.T) { 373 project, zone, apiVersion := "proj1", "us-central1-a", "v1" 374 375 testGetURL := func(_ *http.Client, url string) ([]byte, error) { 376 switch url { 377 case "https://www.googleapis.com/compute/v1/projects/proj1/zones/us-central1-a/instances?filter=status%20eq%20%22RUNNING%22": 378 return readTestJSON(t, "instances.json"), nil 379 case "https://www.googleapis.com/compute/v1/projects/proj1/zones/us-central1-b/instances?filter=status%20eq%20%22RUNNING%22": 380 return []byte("{\"items\": []}"), nil // No instances in us-central1-b 381 case "https://www.googleapis.com/compute/v1/projects/proj1/zones": 382 return readTestJSON(t, "zones.json"), nil 383 } 384 // Return error for non-matching URL. 385 return nil, fmt.Errorf("unknown url: %s", url) 386 } 387 388 il := &gceInstancesLister{ 389 project: project, 390 baseAPIPath: "https://www.googleapis.com/compute/" + apiVersion + "/projects/" + project, 391 getURLFunc: testGetURL, 392 cachePerScope: make(map[string]map[string]*instanceData), 393 namesPerScope: make(map[string][]string), 394 } 395 396 timeBeforeExpand := time.Now().Unix() 397 398 il.expand(time.Second) 399 400 var gotZones []string 401 for z := range il.namesPerScope { 402 gotZones = append(gotZones, z) 403 } 404 // Sort as zones are shuffled in expand. 405 sort.Strings(gotZones) 406 wantZones := []string{"us-central1-a", "us-central1-b"} 407 if !reflect.DeepEqual(gotZones, wantZones) { 408 t.Errorf("got zones=%v, want zones=%v", gotZones, wantZones) 409 } 410 411 gotNames, gotInstances := il.namesPerScope[zone], il.cachePerScope[zone] 412 413 // Expected data comes from the JSON file. 414 wantNames := []string{ 415 "ig-us-central1-a-00-abcd", 416 "ig-us-central1-a-01-efgh", 417 } 418 wantLabels := []map[string]string{ 419 map[string]string{"app": "cloudprober", "shard": "00"}, 420 map[string]string{"app": "cloudprober", "shard": "01"}, 421 } 422 wantNetworks := [][][4]string{ 423 [][4]string{ 424 {"10.0.0.2", "194.197.208.201", "2600:2d00:4030:a47:c0a8:2110:0:0", "2600:2d00:4030:a47:c0a8:2110:1:0"}, 425 {"10.0.0.3", "194.197.208.202", "2600:2d00:4030:a47:c0a8:2110:0:1", ""}, 426 }, 427 [][4]string{{"10.0.1.3", "194.197.209.202", "", ""}}, 428 } 429 430 if !reflect.DeepEqual(gotNames, wantNames) { 431 t.Errorf("Got names=%v, want=%v", gotNames, wantNames) 432 } 433 434 for i, name := range wantNames { 435 ins := gotInstances[name] 436 437 // Check lastupdate timestamp 438 if ins.lastUpdated < timeBeforeExpand { 439 t.Errorf("Instance's last update timestamp (%d) was not updated during expand (timestamp before expand: %d).", ins.lastUpdated, timeBeforeExpand) 440 } 441 442 // Check for labels 443 gotLabels := ins.ii.Labels 444 wantLabels := wantLabels[i] 445 if !reflect.DeepEqual(gotLabels, wantLabels) { 446 t.Errorf("Got labels=%v, want labels=%v", gotLabels, wantLabels) 447 } 448 449 // Check for ips 450 if len(ins.ii.NetworkInterfaces) != len(wantNetworks[i]) { 451 t.Errorf("Got %d nics, want %d", len(ins.ii.NetworkInterfaces), len(wantNetworks[i])) 452 } 453 for i, wantIPs := range wantNetworks[i] { 454 nic := ins.ii.NetworkInterfaces[i] 455 gotIPs := [4]string{nic.NetworkIP, nic.AccessConfigs[0].NatIP, nic.Ipv6Address} 456 if len(nic.Ipv6AccessConfigs) > 0 { 457 gotIPs[3] = nic.Ipv6AccessConfigs[0].ExternalIpv6 458 } 459 if gotIPs != wantIPs { 460 t.Errorf("Got ips=%v, want=%v", gotIPs, wantIPs) 461 } 462 } 463 } 464 }