github.com/mponton/terratest@v0.44.0/modules/gcp/compute.go (about) 1 package gcp 2 3 import ( 4 "context" 5 "fmt" 6 "net/http" 7 "path" 8 "strings" 9 "time" 10 11 "github.com/mponton/terratest/modules/retry" 12 "google.golang.org/api/compute/v1" 13 14 "github.com/mponton/terratest/modules/logger" 15 "github.com/mponton/terratest/modules/random" 16 "github.com/mponton/terratest/modules/testing" 17 "golang.org/x/oauth2/google" 18 ) 19 20 // Corresponds to a GCP Compute Instance (https://cloud.google.com/compute/docs/instances/) 21 type Instance struct { 22 projectID string 23 *compute.Instance 24 } 25 26 // Corresponds to a GCP Image (https://cloud.google.com/compute/docs/images) 27 type Image struct { 28 projectID string 29 *compute.Image 30 } 31 32 // Corresponds to a GCP Zonal Instance Group (https://cloud.google.com/compute/docs/instance-groups/) 33 type ZonalInstanceGroup struct { 34 projectID string 35 *compute.InstanceGroup 36 } 37 38 // Corresponds to a GCP Regional Instance Group (https://cloud.google.com/compute/docs/instance-groups/) 39 type RegionalInstanceGroup struct { 40 projectID string 41 *compute.InstanceGroup 42 } 43 44 type InstanceGroup interface { 45 GetInstanceIds(t testing.TestingT) []string 46 GetInstanceIdsE(t testing.TestingT) ([]string, error) 47 } 48 49 // FetchInstance queries GCP to return an instance of the (GCP Compute) Instance type 50 func FetchInstance(t testing.TestingT, projectID string, name string) *Instance { 51 instance, err := FetchInstanceE(t, projectID, name) 52 if err != nil { 53 t.Fatal(err) 54 } 55 56 return instance 57 } 58 59 // FetchInstance queries GCP to return an instance of the (GCP Compute) Instance type 60 func FetchInstanceE(t testing.TestingT, projectID string, name string) (*Instance, error) { 61 logger.Logf(t, "Getting Compute Instance %s", name) 62 63 ctx := context.Background() 64 service, err := NewComputeServiceE(t) 65 if err != nil { 66 t.Fatal(err) 67 } 68 69 // If we want to fetch an Instance without knowing its Zone, we have to query GCP for all Instances in the project 70 // and match on name. 71 instanceAggregatedList, err := service.Instances.AggregatedList(projectID).Context(ctx).Do() 72 if err != nil { 73 return nil, fmt.Errorf("Instances.AggregatedList(%s) got error: %v", projectID, err) 74 } 75 76 for _, instanceList := range instanceAggregatedList.Items { 77 for _, instance := range instanceList.Instances { 78 if name == instance.Name { 79 return &Instance{projectID, instance}, nil 80 } 81 } 82 } 83 84 return nil, fmt.Errorf("Compute Instance %s could not be found in project %s", name, projectID) 85 } 86 87 // FetchImage queries GCP to return a new instance of the (GCP Compute) Image type 88 func FetchImage(t testing.TestingT, projectID string, name string) *Image { 89 image, err := FetchImageE(t, projectID, name) 90 if err != nil { 91 t.Fatal(err) 92 } 93 94 return image 95 } 96 97 // FetchImage queries GCP to return a new instance of the (GCP Compute) Image type 98 func FetchImageE(t testing.TestingT, projectID string, name string) (*Image, error) { 99 logger.Logf(t, "Getting Image %s", name) 100 101 ctx := context.Background() 102 service, err := NewComputeServiceE(t) 103 if err != nil { 104 return nil, err 105 } 106 107 req := service.Images.Get(projectID, name) 108 image, err := req.Context(ctx).Do() 109 if err != nil { 110 return nil, err 111 } 112 113 return &Image{projectID, image}, nil 114 } 115 116 // FetchRegionalInstanceGroup queries GCP to return a new instance of the Regional Instance Group type 117 func FetchRegionalInstanceGroup(t testing.TestingT, projectID string, region string, name string) *RegionalInstanceGroup { 118 instanceGroup, err := FetchRegionalInstanceGroupE(t, projectID, region, name) 119 if err != nil { 120 t.Fatal(err) 121 } 122 123 return instanceGroup 124 } 125 126 // FetchRegionalInstanceGroup queries GCP to return a new instance of the Regional Instance Group type 127 func FetchRegionalInstanceGroupE(t testing.TestingT, projectID string, region string, name string) (*RegionalInstanceGroup, error) { 128 logger.Logf(t, "Getting Regional Instance Group %s", name) 129 130 ctx := context.Background() 131 service, err := NewComputeServiceE(t) 132 if err != nil { 133 return nil, err 134 } 135 136 req := service.RegionInstanceGroups.Get(projectID, region, name) 137 instanceGroup, err := req.Context(ctx).Do() 138 if err != nil { 139 return nil, err 140 } 141 142 return &RegionalInstanceGroup{projectID, instanceGroup}, nil 143 } 144 145 // FetchZonalInstanceGroup queries GCP to return a new instance of the Regional Instance Group type 146 func FetchZonalInstanceGroup(t testing.TestingT, projectID string, zone string, name string) *ZonalInstanceGroup { 147 instanceGroup, err := FetchZonalInstanceGroupE(t, projectID, zone, name) 148 if err != nil { 149 t.Fatal(err) 150 } 151 152 return instanceGroup 153 } 154 155 // FetchZonalInstanceGroup queries GCP to return a new instance of the Regional Instance Group type 156 func FetchZonalInstanceGroupE(t testing.TestingT, projectID string, zone string, name string) (*ZonalInstanceGroup, error) { 157 logger.Logf(t, "Getting Zonal Instance Group %s", name) 158 159 ctx := context.Background() 160 service, err := NewComputeServiceE(t) 161 if err != nil { 162 return nil, err 163 } 164 165 req := service.InstanceGroups.Get(projectID, zone, name) 166 instanceGroup, err := req.Context(ctx).Do() 167 if err != nil { 168 return nil, err 169 } 170 171 return &ZonalInstanceGroup{projectID, instanceGroup}, nil 172 } 173 174 // GetPublicIP gets the public IP address of the given Compute Instance. 175 func (i *Instance) GetPublicIp(t testing.TestingT) string { 176 ip, err := i.GetPublicIpE(t) 177 if err != nil { 178 t.Fatal(err) 179 } 180 return ip 181 } 182 183 // GetPublicIpE gets the public IP address of the given Compute Instance. 184 func (i *Instance) GetPublicIpE(t testing.TestingT) (string, error) { 185 // If there are no accessConfigs specified, then this instance will have no external internet access: 186 // https://cloud.google.com/compute/docs/reference/rest/v1/instances. 187 if len(i.NetworkInterfaces[0].AccessConfigs) == 0 { 188 return "", fmt.Errorf("Attempted to get public IP of Compute Instance %s, but that Compute Instance does not have a public IP address", i.Name) 189 } 190 191 ip := i.NetworkInterfaces[0].AccessConfigs[0].NatIP 192 193 return ip, nil 194 } 195 196 // GetLabels returns all the tags for the given Compute Instance. 197 func (i *Instance) GetLabels(t testing.TestingT) map[string]string { 198 return i.Labels 199 } 200 201 // GetZone returns the Zone in which the Compute Instance is located. 202 func (i *Instance) GetZone(t testing.TestingT) string { 203 return ZoneUrlToZone(i.Zone) 204 } 205 206 // SetLabels adds the tags to the given Compute Instance. 207 func (i *Instance) SetLabels(t testing.TestingT, labels map[string]string) { 208 err := i.SetLabelsE(t, labels) 209 if err != nil { 210 t.Fatal(err) 211 } 212 } 213 214 // SetLabelsE adds the tags to the given Compute Instance. 215 func (i *Instance) SetLabelsE(t testing.TestingT, labels map[string]string) error { 216 logger.Logf(t, "Adding labels to instance %s in zone %s", i.Name, i.Zone) 217 218 ctx := context.Background() 219 service, err := NewComputeServiceE(t) 220 if err != nil { 221 return err 222 } 223 224 req := compute.InstancesSetLabelsRequest{Labels: labels, LabelFingerprint: i.LabelFingerprint} 225 if _, err := service.Instances.SetLabels(i.projectID, i.GetZone(t), i.Name, &req).Context(ctx).Do(); err != nil { 226 return fmt.Errorf("Instances.SetLabels(%s) got error: %v", i.Name, err) 227 } 228 229 return nil 230 } 231 232 // GetMetadata gets the given Compute Instance's metadata 233 func (i *Instance) GetMetadata(t testing.TestingT) []*compute.MetadataItems { 234 return i.Metadata.Items 235 } 236 237 // SetMetadata sets the given Compute Instance's metadata 238 func (i *Instance) SetMetadata(t testing.TestingT, metadata map[string]string) { 239 err := i.SetMetadataE(t, metadata) 240 if err != nil { 241 t.Fatal(err) 242 } 243 } 244 245 // SetLabelsE adds the given metadata map to the existing metadata of the given Compute Instance. 246 func (i *Instance) SetMetadataE(t testing.TestingT, metadata map[string]string) error { 247 logger.Logf(t, "Adding metadata to instance %s in zone %s", i.Name, i.Zone) 248 249 ctx := context.Background() 250 service, err := NewInstancesServiceE(t) 251 if err != nil { 252 return err 253 } 254 255 metadataItems := newMetadata(t, i.Metadata, metadata) 256 req := service.SetMetadata(i.projectID, i.GetZone(t), i.Name, metadataItems) 257 if _, err := req.Context(ctx).Do(); err != nil { 258 return fmt.Errorf("Instances.SetMetadata(%s) got error: %v", i.Name, err) 259 } 260 261 return nil 262 } 263 264 // newMetadata takes in a Compute Instance's existing metadata plus a new set of key-value pairs and returns an updated 265 // metadata object. 266 func newMetadata(t testing.TestingT, oldMetadata *compute.Metadata, kvs map[string]string) *compute.Metadata { 267 items := []*compute.MetadataItems{} 268 269 for key, val := range kvs { 270 item := &compute.MetadataItems{ 271 Key: key, 272 Value: &val, 273 } 274 275 items = append(oldMetadata.Items, item) 276 } 277 278 newMetadata := &compute.Metadata{ 279 Fingerprint: oldMetadata.Fingerprint, 280 Items: items, 281 } 282 283 return newMetadata 284 } 285 286 // Add the given public SSH key to the Compute Instance. Users can SSH in with the given username. 287 func (i *Instance) AddSshKey(t testing.TestingT, username string, publicKey string) { 288 err := i.AddSshKeyE(t, username, publicKey) 289 if err != nil { 290 t.Fatal(err) 291 } 292 } 293 294 // Add the given public SSH key to the Compute Instance. Users can SSH in with the given username. 295 func (i *Instance) AddSshKeyE(t testing.TestingT, username string, publicKey string) error { 296 logger.Logf(t, "Adding SSH Key to Compute Instance %s for username %s\n", i.Name, username) 297 298 // We represent the key in the format required per GCP docs (https://cloud.google.com/compute/docs/instances/adding-removing-ssh-keys) 299 publicKeyFormatted := strings.TrimSpace(publicKey) 300 sshKeyFormatted := fmt.Sprintf("%s:%s %s", username, publicKeyFormatted, username) 301 302 metadata := map[string]string{ 303 "ssh-keys": sshKeyFormatted, 304 } 305 306 err := i.SetMetadataE(t, metadata) 307 if err != nil { 308 return fmt.Errorf("Failed to add SSH key to Compute Instance: %s", err) 309 } 310 311 return nil 312 } 313 314 // DeleteImage deletes the given Compute Image. 315 func (i *Image) DeleteImage(t testing.TestingT) { 316 err := i.DeleteImageE(t) 317 if err != nil { 318 t.Fatal(err) 319 } 320 } 321 322 // DeleteImageE deletes the given Compute Image. 323 func (i *Image) DeleteImageE(t testing.TestingT) error { 324 logger.Logf(t, "Destroying Image %s", i.Name) 325 326 ctx := context.Background() 327 service, err := NewComputeServiceE(t) 328 if err != nil { 329 return err 330 } 331 332 if _, err := service.Images.Delete(i.projectID, i.Name).Context(ctx).Do(); err != nil { 333 return fmt.Errorf("Images.Delete(%s) got error: %v", i.Name, err) 334 } 335 336 return nil 337 } 338 339 // GetInstanceIds gets the IDs of Instances in the given Instance Group. 340 func (ig *ZonalInstanceGroup) GetInstanceIds(t testing.TestingT) []string { 341 ids, err := ig.GetInstanceIdsE(t) 342 if err != nil { 343 t.Fatal(err) 344 } 345 return ids 346 } 347 348 // GetInstanceIdsE gets the IDs of Instances in the given Zonal Instance Group. 349 func (ig *ZonalInstanceGroup) GetInstanceIdsE(t testing.TestingT) ([]string, error) { 350 logger.Logf(t, "Get instances for Zonal Instance Group %s", ig.Name) 351 352 ctx := context.Background() 353 service, err := NewComputeServiceE(t) 354 if err != nil { 355 return nil, err 356 } 357 358 requestBody := &compute.InstanceGroupsListInstancesRequest{ 359 InstanceState: "ALL", 360 } 361 362 instanceIDs := []string{} 363 zone := ZoneUrlToZone(ig.Zone) 364 365 req := service.InstanceGroups.ListInstances(ig.projectID, zone, ig.Name, requestBody) 366 367 err = req.Pages(ctx, func(page *compute.InstanceGroupsListInstances) error { 368 for _, instance := range page.Items { 369 // For some reason service.InstanceGroups.ListInstances returns us a collection 370 // with Instance URLs and we need only the Instance ID for the next call. Use 371 // the path functions to chop the Instance ID off the end of the URL. 372 instanceID := path.Base(instance.Instance) 373 instanceIDs = append(instanceIDs, instanceID) 374 } 375 return nil 376 }) 377 if err != nil { 378 return nil, fmt.Errorf("InstanceGroups.ListInstances(%s) got error: %v", ig.Name, err) 379 } 380 381 return instanceIDs, nil 382 } 383 384 // GetInstanceIds gets the IDs of Instances in the given Regional Instance Group. 385 func (ig *RegionalInstanceGroup) GetInstanceIds(t testing.TestingT) []string { 386 ids, err := ig.GetInstanceIdsE(t) 387 if err != nil { 388 t.Fatal(err) 389 } 390 return ids 391 } 392 393 // GetInstanceIdsE gets the IDs of Instances in the given Regional Instance Group. 394 func (ig *RegionalInstanceGroup) GetInstanceIdsE(t testing.TestingT) ([]string, error) { 395 logger.Logf(t, "Get instances for Regional Instance Group %s", ig.Name) 396 397 ctx := context.Background() 398 399 service, err := NewComputeServiceE(t) 400 if err != nil { 401 return nil, err 402 } 403 404 requestBody := &compute.RegionInstanceGroupsListInstancesRequest{ 405 InstanceState: "ALL", 406 } 407 408 instanceIDs := []string{} 409 region := RegionUrlToRegion(ig.Region) 410 411 req := service.RegionInstanceGroups.ListInstances(ig.projectID, region, ig.Name, requestBody) 412 413 err = req.Pages(ctx, func(page *compute.RegionInstanceGroupsListInstances) error { 414 for _, instance := range page.Items { 415 // For some reason service.InstanceGroups.ListInstances returns us a collection 416 // with Instance URLs and we need only the Instance ID for the next call. Use 417 // the path functions to chop the Instance ID off the end of the URL. 418 instanceID := path.Base(instance.Instance) 419 instanceIDs = append(instanceIDs, instanceID) 420 } 421 return nil 422 }) 423 if err != nil { 424 return nil, fmt.Errorf("InstanceGroups.ListInstances(%s) got error: %v", ig.Name, err) 425 } 426 427 return instanceIDs, nil 428 } 429 430 // Return a collection of Instance structs from the given Instance Group 431 func (ig *ZonalInstanceGroup) GetInstances(t testing.TestingT, projectId string) []*Instance { 432 return getInstances(t, ig, projectId) 433 } 434 435 // Return a collection of Instance structs from the given Instance Group 436 func (ig *ZonalInstanceGroup) GetInstancesE(t testing.TestingT, projectId string) ([]*Instance, error) { 437 return getInstancesE(t, ig, projectId) 438 } 439 440 // Return a collection of Instance structs from the given Instance Group 441 func (ig *RegionalInstanceGroup) GetInstances(t testing.TestingT, projectId string) []*Instance { 442 return getInstances(t, ig, projectId) 443 } 444 445 // Return a collection of Instance structs from the given Instance Group 446 func (ig *RegionalInstanceGroup) GetInstancesE(t testing.TestingT, projectId string) ([]*Instance, error) { 447 return getInstancesE(t, ig, projectId) 448 } 449 450 // getInstancesE returns a collection of Instance structs from the given Instance Group 451 func getInstances(t testing.TestingT, ig InstanceGroup, projectId string) []*Instance { 452 instances, err := getInstancesE(t, ig, projectId) 453 if err != nil { 454 t.Fatal(err) 455 } 456 457 return instances 458 } 459 460 // getInstancesE returns a collection of Instance structs from the given Instance Group 461 func getInstancesE(t testing.TestingT, ig InstanceGroup, projectId string) ([]*Instance, error) { 462 instanceIds, err := ig.GetInstanceIdsE(t) 463 if err != nil { 464 return nil, fmt.Errorf("Failed to get Instance Group IDs: %s", err) 465 } 466 467 var instances []*Instance 468 469 for _, instanceId := range instanceIds { 470 instance, err := FetchInstanceE(t, projectId, instanceId) 471 if err != nil { 472 return nil, fmt.Errorf("Failed to get Instance: %s", err) 473 } 474 475 instances = append(instances, instance) 476 } 477 478 return instances, nil 479 } 480 481 // GetPublicIps returns a slice of the public IPs from the given Instance Group 482 func (ig *ZonalInstanceGroup) GetPublicIps(t testing.TestingT, projectId string) []string { 483 return getPublicIps(t, ig, projectId) 484 } 485 486 // GetPublicIpsE returns a slice of the public IPs from the given Instance Group 487 func (ig *ZonalInstanceGroup) GetPublicIpsE(t testing.TestingT, projectId string) ([]string, error) { 488 return getPublicIpsE(t, ig, projectId) 489 } 490 491 // GetPublicIps returns a slice of the public IPs from the given Instance Group 492 func (ig *RegionalInstanceGroup) GetPublicIps(t testing.TestingT, projectId string) []string { 493 return getPublicIps(t, ig, projectId) 494 } 495 496 // GetPublicIpsE returns a slice of the public IPs from the given Instance Group 497 func (ig *RegionalInstanceGroup) GetPublicIpsE(t testing.TestingT, projectId string) ([]string, error) { 498 return getPublicIpsE(t, ig, projectId) 499 } 500 501 // getPublicIps a slice of the public IPs from the given Instance Group 502 func getPublicIps(t testing.TestingT, ig InstanceGroup, projectId string) []string { 503 ips, err := getPublicIpsE(t, ig, projectId) 504 if err != nil { 505 t.Fatal(err) 506 } 507 508 return ips 509 } 510 511 // getPublicIpsE a slice of the public IPs from the given Instance Group 512 func getPublicIpsE(t testing.TestingT, ig InstanceGroup, projectId string) ([]string, error) { 513 instances, err := getInstancesE(t, ig, projectId) 514 if err != nil { 515 return nil, fmt.Errorf("Failed to get Compute Instances from Instance Group: %s", err) 516 } 517 518 var ips []string 519 520 for _, instance := range instances { 521 ip := instance.GetPublicIp(t) 522 ips = append(ips, ip) 523 } 524 525 return ips, nil 526 } 527 528 // getRandomInstance returns a randomly selected Instance from the Regional Instance Group 529 func (ig *ZonalInstanceGroup) GetRandomInstance(t testing.TestingT) *Instance { 530 return getRandomInstance(t, ig, ig.Name, ig.Region, ig.Size, ig.projectID) 531 } 532 533 // getRandomInstanceE returns a randomly selected Instance from the Regional Instance Group 534 func (ig *ZonalInstanceGroup) GetRandomInstanceE(t testing.TestingT) (*Instance, error) { 535 return getRandomInstanceE(t, ig, ig.Name, ig.Region, ig.Size, ig.projectID) 536 } 537 538 // getRandomInstance returns a randomly selected Instance from the Regional Instance Group 539 func (ig *RegionalInstanceGroup) GetRandomInstance(t testing.TestingT) *Instance { 540 return getRandomInstance(t, ig, ig.Name, ig.Region, ig.Size, ig.projectID) 541 } 542 543 // getRandomInstanceE returns a randomly selected Instance from the Regional Instance Group 544 func (ig *RegionalInstanceGroup) GetRandomInstanceE(t testing.TestingT) (*Instance, error) { 545 return getRandomInstanceE(t, ig, ig.Name, ig.Region, ig.Size, ig.projectID) 546 } 547 548 func getRandomInstance(t testing.TestingT, ig InstanceGroup, name string, region string, size int64, projectID string) *Instance { 549 instance, err := getRandomInstanceE(t, ig, name, region, size, projectID) 550 if err != nil { 551 t.Fatal(err) 552 } 553 554 return instance 555 } 556 557 func getRandomInstanceE(t testing.TestingT, ig InstanceGroup, name string, region string, size int64, projectID string) (*Instance, error) { 558 instanceIDs := ig.GetInstanceIds(t) 559 if len(instanceIDs) == 0 { 560 return nil, fmt.Errorf("Could not find any instances in Regional Instance Group or Zonal Instance Group %s in Region %s", name, region) 561 } 562 563 clusterSize := int(size) 564 if len(instanceIDs) != clusterSize { 565 return nil, fmt.Errorf("Expected Regional Instance Group or Zonal Instance Group %s in Region %s to have %d instances, but found %d", name, region, clusterSize, len(instanceIDs)) 566 } 567 568 randIndex := random.Random(0, clusterSize-1) 569 instanceID := instanceIDs[randIndex] 570 instance := FetchInstance(t, projectID, instanceID) 571 572 return instance, nil 573 } 574 575 // NewComputeService creates a new Compute service, which is used to make GCE API calls. 576 func NewComputeService(t testing.TestingT) *compute.Service { 577 client, err := NewComputeServiceE(t) 578 if err != nil { 579 t.Fatal(err) 580 } 581 return client 582 } 583 584 // NewComputeServiceE creates a new Compute service, which is used to make GCE API calls. 585 func NewComputeServiceE(t testing.TestingT) (*compute.Service, error) { 586 ctx := context.Background() 587 588 // Retrieve the Google OAuth token using a retry loop as it can sometimes return an error. 589 // e.g: oauth2: cannot fetch token: Post https://oauth2.googleapis.com/token: net/http: TLS handshake timeout 590 // This is loosely based on https://github.com/kubernetes/kubernetes/blob/7e8de5422cb5ad76dd0c147cf4336220d282e34b/pkg/cloudprovider/providers/gce/gce.go#L831. 591 592 description := "Attempting to request a Google OAuth2 token" 593 maxRetries := 6 594 timeBetweenRetries := 10 * time.Second 595 596 var client *http.Client 597 598 msg, retryErr := retry.DoWithRetryE(t, description, maxRetries, timeBetweenRetries, func() (string, error) { 599 rawClient, err := google.DefaultClient(ctx, compute.CloudPlatformScope) 600 if err != nil { 601 return "Error retrieving default GCP client", err 602 } 603 client = rawClient 604 return "Successfully retrieved default GCP client", nil 605 }) 606 logger.Logf(t, msg) 607 608 if retryErr != nil { 609 return nil, retryErr 610 } 611 612 return compute.New(client) 613 } 614 615 // NewInstancesService creates a new InstancesService service, which is used to make a subset of GCE API calls. 616 func NewInstancesService(t testing.TestingT) *compute.InstancesService { 617 client, err := NewInstancesServiceE(t) 618 if err != nil { 619 t.Fatal(err) 620 } 621 return client 622 } 623 624 // NewInstancesServiceE creates a new InstancesService service, which is used to make a subset of GCE API calls. 625 func NewInstancesServiceE(t testing.TestingT) (*compute.InstancesService, error) { 626 service, err := NewComputeServiceE(t) 627 if err != nil { 628 return nil, fmt.Errorf("Failed to get new Instances Service\n") 629 } 630 631 return service.Instances, nil 632 } 633 634 // Return a random, valid name for GCP resources. Many resources in GCP requires lowercase letters only. 635 func RandomValidGcpName() string { 636 id := strings.ToLower(random.UniqueId()) 637 instanceName := fmt.Sprintf("terratest-%s", id) 638 639 return instanceName 640 }