github.com/alexissmirnov/terraform@v0.4.3-0.20150423153700-1ef9731a2f14/builtin/providers/google/resource_compute_instance.go (about) 1 package google 2 3 import ( 4 "fmt" 5 "log" 6 "time" 7 8 "github.com/hashicorp/terraform/helper/hashcode" 9 "github.com/hashicorp/terraform/helper/schema" 10 "google.golang.org/api/compute/v1" 11 "google.golang.org/api/googleapi" 12 ) 13 14 func resourceComputeInstance() *schema.Resource { 15 return &schema.Resource{ 16 Create: resourceComputeInstanceCreate, 17 Read: resourceComputeInstanceRead, 18 Update: resourceComputeInstanceUpdate, 19 Delete: resourceComputeInstanceDelete, 20 21 SchemaVersion: 1, 22 MigrateState: resourceComputeInstanceMigrateState, 23 24 Schema: map[string]*schema.Schema{ 25 "name": &schema.Schema{ 26 Type: schema.TypeString, 27 Required: true, 28 ForceNew: true, 29 }, 30 31 "description": &schema.Schema{ 32 Type: schema.TypeString, 33 Optional: true, 34 ForceNew: true, 35 }, 36 37 "machine_type": &schema.Schema{ 38 Type: schema.TypeString, 39 Required: true, 40 ForceNew: true, 41 }, 42 43 "zone": &schema.Schema{ 44 Type: schema.TypeString, 45 Required: true, 46 ForceNew: true, 47 }, 48 49 "disk": &schema.Schema{ 50 Type: schema.TypeList, 51 Required: true, 52 ForceNew: true, 53 Elem: &schema.Resource{ 54 Schema: map[string]*schema.Schema{ 55 // TODO(mitchellh): one of image or disk is required 56 57 "disk": &schema.Schema{ 58 Type: schema.TypeString, 59 Optional: true, 60 ForceNew: true, 61 }, 62 63 "image": &schema.Schema{ 64 Type: schema.TypeString, 65 Optional: true, 66 ForceNew: true, 67 }, 68 69 "type": &schema.Schema{ 70 Type: schema.TypeString, 71 Optional: true, 72 ForceNew: true, 73 }, 74 75 "auto_delete": &schema.Schema{ 76 Type: schema.TypeBool, 77 Optional: true, 78 Default: true, 79 ForceNew: true, 80 }, 81 82 "size": &schema.Schema{ 83 Type: schema.TypeInt, 84 Optional: true, 85 ForceNew: true, 86 }, 87 88 "device_name": &schema.Schema{ 89 Type: schema.TypeString, 90 Optional: true, 91 }, 92 }, 93 }, 94 }, 95 96 "network_interface": &schema.Schema{ 97 Type: schema.TypeList, 98 Optional: true, 99 ForceNew: true, 100 Elem: &schema.Resource{ 101 Schema: map[string]*schema.Schema{ 102 "network": &schema.Schema{ 103 Type: schema.TypeString, 104 Required: true, 105 ForceNew: true, 106 }, 107 108 "name": &schema.Schema{ 109 Type: schema.TypeString, 110 Computed: true, 111 }, 112 113 "address": &schema.Schema{ 114 Type: schema.TypeString, 115 Computed: true, 116 }, 117 118 "access_config": &schema.Schema{ 119 Type: schema.TypeList, 120 Optional: true, 121 Elem: &schema.Resource{ 122 Schema: map[string]*schema.Schema{ 123 "nat_ip": &schema.Schema{ 124 Type: schema.TypeString, 125 Computed: true, 126 Optional: true, 127 }, 128 }, 129 }, 130 }, 131 }, 132 }, 133 }, 134 135 "network": &schema.Schema{ 136 Type: schema.TypeList, 137 Optional: true, 138 ForceNew: true, 139 Deprecated: "Please use network_interface", 140 Elem: &schema.Resource{ 141 Schema: map[string]*schema.Schema{ 142 "source": &schema.Schema{ 143 Type: schema.TypeString, 144 Required: true, 145 ForceNew: true, 146 }, 147 148 "address": &schema.Schema{ 149 Type: schema.TypeString, 150 Optional: true, 151 ForceNew: true, 152 }, 153 154 "name": &schema.Schema{ 155 Type: schema.TypeString, 156 Computed: true, 157 }, 158 159 "internal_address": &schema.Schema{ 160 Type: schema.TypeString, 161 Computed: true, 162 }, 163 164 "external_address": &schema.Schema{ 165 Type: schema.TypeString, 166 Computed: true, 167 }, 168 }, 169 }, 170 }, 171 172 "can_ip_forward": &schema.Schema{ 173 Type: schema.TypeBool, 174 Optional: true, 175 Default: false, 176 ForceNew: true, 177 }, 178 179 "metadata": &schema.Schema{ 180 Type: schema.TypeMap, 181 Optional: true, 182 Elem: schema.TypeString, 183 }, 184 185 "service_account": &schema.Schema{ 186 Type: schema.TypeList, 187 Optional: true, 188 ForceNew: true, 189 Elem: &schema.Resource{ 190 Schema: map[string]*schema.Schema{ 191 "email": &schema.Schema{ 192 Type: schema.TypeString, 193 Computed: true, 194 ForceNew: true, 195 }, 196 197 "scopes": &schema.Schema{ 198 Type: schema.TypeList, 199 Required: true, 200 ForceNew: true, 201 Elem: &schema.Schema{ 202 Type: schema.TypeString, 203 StateFunc: func(v interface{}) string { 204 return canonicalizeServiceScope(v.(string)) 205 }, 206 }, 207 }, 208 }, 209 }, 210 }, 211 212 "tags": &schema.Schema{ 213 Type: schema.TypeSet, 214 Optional: true, 215 Elem: &schema.Schema{Type: schema.TypeString}, 216 Set: func(v interface{}) int { 217 return hashcode.String(v.(string)) 218 }, 219 }, 220 221 "metadata_fingerprint": &schema.Schema{ 222 Type: schema.TypeString, 223 Computed: true, 224 }, 225 226 "tags_fingerprint": &schema.Schema{ 227 Type: schema.TypeString, 228 Computed: true, 229 }, 230 231 "self_link": &schema.Schema{ 232 Type: schema.TypeString, 233 Computed: true, 234 }, 235 }, 236 } 237 } 238 239 func resourceOperationWaitZone( 240 config *Config, op *compute.Operation, zone string, activity string) error { 241 242 w := &OperationWaiter{ 243 Service: config.clientCompute, 244 Op: op, 245 Project: config.Project, 246 Zone: zone, 247 Type: OperationWaitZone, 248 } 249 state := w.Conf() 250 state.Delay = 10 * time.Second 251 state.Timeout = 10 * time.Minute 252 state.MinTimeout = 2 * time.Second 253 opRaw, err := state.WaitForState() 254 if err != nil { 255 return fmt.Errorf("Error waiting for %s: %s", activity, err) 256 } 257 op = opRaw.(*compute.Operation) 258 if op.Error != nil { 259 // Return the error 260 return OperationError(*op.Error) 261 } 262 return nil 263 } 264 265 func resourceComputeInstanceCreate(d *schema.ResourceData, meta interface{}) error { 266 config := meta.(*Config) 267 268 // Get the zone 269 log.Printf("[DEBUG] Loading zone: %s", d.Get("zone").(string)) 270 zone, err := config.clientCompute.Zones.Get( 271 config.Project, d.Get("zone").(string)).Do() 272 if err != nil { 273 return fmt.Errorf( 274 "Error loading zone '%s': %s", d.Get("zone").(string), err) 275 } 276 277 // Get the machine type 278 log.Printf("[DEBUG] Loading machine type: %s", d.Get("machine_type").(string)) 279 machineType, err := config.clientCompute.MachineTypes.Get( 280 config.Project, zone.Name, d.Get("machine_type").(string)).Do() 281 if err != nil { 282 return fmt.Errorf( 283 "Error loading machine type: %s", 284 err) 285 } 286 287 // Build up the list of disks 288 disksCount := d.Get("disk.#").(int) 289 disks := make([]*compute.AttachedDisk, 0, disksCount) 290 for i := 0; i < disksCount; i++ { 291 prefix := fmt.Sprintf("disk.%d", i) 292 293 // var sourceLink string 294 295 // Build the disk 296 var disk compute.AttachedDisk 297 disk.Type = "PERSISTENT" 298 disk.Mode = "READ_WRITE" 299 disk.Boot = i == 0 300 disk.AutoDelete = d.Get(prefix + ".auto_delete").(bool) 301 302 // Load up the disk for this disk if specified 303 if v, ok := d.GetOk(prefix + ".disk"); ok { 304 diskName := v.(string) 305 diskData, err := config.clientCompute.Disks.Get( 306 config.Project, zone.Name, diskName).Do() 307 if err != nil { 308 return fmt.Errorf( 309 "Error loading disk '%s': %s", 310 diskName, err) 311 } 312 313 disk.Source = diskData.SelfLink 314 } 315 316 // Load up the image for this disk if specified 317 if v, ok := d.GetOk(prefix + ".image"); ok { 318 imageName := v.(string) 319 320 imageUrl, err := resolveImage(config, imageName) 321 if err != nil { 322 return fmt.Errorf( 323 "Error resolving image name '%s': %s", 324 imageName, err) 325 } 326 327 disk.InitializeParams = &compute.AttachedDiskInitializeParams{ 328 SourceImage: imageUrl, 329 } 330 } 331 332 if v, ok := d.GetOk(prefix + ".type"); ok { 333 diskTypeName := v.(string) 334 diskType, err := readDiskType(config, zone, diskTypeName) 335 if err != nil { 336 return fmt.Errorf( 337 "Error loading disk type '%s': %s", 338 diskTypeName, err) 339 } 340 341 disk.InitializeParams.DiskType = diskType.SelfLink 342 } 343 344 if v, ok := d.GetOk(prefix + ".size"); ok { 345 diskSizeGb := v.(int) 346 disk.InitializeParams.DiskSizeGb = int64(diskSizeGb) 347 } 348 349 if v, ok := d.GetOk(prefix + ".device_name"); ok { 350 disk.DeviceName = v.(string) 351 } 352 353 disks = append(disks, &disk) 354 } 355 356 networksCount := d.Get("network.#").(int) 357 networkInterfacesCount := d.Get("network_interface.#").(int) 358 359 if networksCount > 0 && networkInterfacesCount > 0 { 360 return fmt.Errorf("Error: cannot define both networks and network_interfaces.") 361 } 362 if networksCount == 0 && networkInterfacesCount == 0 { 363 return fmt.Errorf("Error: Must define at least one network_interface.") 364 } 365 366 var networkInterfaces []*compute.NetworkInterface 367 368 if networksCount > 0 { 369 // TODO: Delete this block when removing network { } 370 // Build up the list of networkInterfaces 371 networkInterfaces = make([]*compute.NetworkInterface, 0, networksCount) 372 for i := 0; i < networksCount; i++ { 373 prefix := fmt.Sprintf("network.%d", i) 374 // Load up the name of this network 375 networkName := d.Get(prefix + ".source").(string) 376 network, err := config.clientCompute.Networks.Get( 377 config.Project, networkName).Do() 378 if err != nil { 379 return fmt.Errorf( 380 "Error loading network '%s': %s", 381 networkName, err) 382 } 383 384 // Build the networkInterface 385 var iface compute.NetworkInterface 386 iface.AccessConfigs = []*compute.AccessConfig{ 387 &compute.AccessConfig{ 388 Type: "ONE_TO_ONE_NAT", 389 NatIP: d.Get(prefix + ".address").(string), 390 }, 391 } 392 iface.Network = network.SelfLink 393 394 networkInterfaces = append(networkInterfaces, &iface) 395 } 396 } 397 398 if networkInterfacesCount > 0 { 399 // Build up the list of networkInterfaces 400 networkInterfaces = make([]*compute.NetworkInterface, 0, networkInterfacesCount) 401 for i := 0; i < networkInterfacesCount; i++ { 402 prefix := fmt.Sprintf("network_interface.%d", i) 403 // Load up the name of this network_interfac 404 networkName := d.Get(prefix + ".network").(string) 405 network, err := config.clientCompute.Networks.Get( 406 config.Project, networkName).Do() 407 if err != nil { 408 return fmt.Errorf( 409 "Error referencing network '%s': %s", 410 networkName, err) 411 } 412 413 // Build the networkInterface 414 var iface compute.NetworkInterface 415 iface.Network = network.SelfLink 416 417 // Handle access_config structs 418 accessConfigsCount := d.Get(prefix + ".access_config.#").(int) 419 iface.AccessConfigs = make([]*compute.AccessConfig, accessConfigsCount) 420 for j := 0; j < accessConfigsCount; j++ { 421 acPrefix := fmt.Sprintf("%s.access_config.%d", prefix, j) 422 iface.AccessConfigs[j] = &compute.AccessConfig{ 423 Type: "ONE_TO_ONE_NAT", 424 NatIP: d.Get(acPrefix + ".nat_ip").(string), 425 } 426 } 427 428 networkInterfaces = append(networkInterfaces, &iface) 429 } 430 } 431 432 serviceAccountsCount := d.Get("service_account.#").(int) 433 serviceAccounts := make([]*compute.ServiceAccount, 0, serviceAccountsCount) 434 for i := 0; i < serviceAccountsCount; i++ { 435 prefix := fmt.Sprintf("service_account.%d", i) 436 437 scopesCount := d.Get(prefix + ".scopes.#").(int) 438 scopes := make([]string, 0, scopesCount) 439 for j := 0; j < scopesCount; j++ { 440 scope := d.Get(fmt.Sprintf(prefix+".scopes.%d", j)).(string) 441 scopes = append(scopes, canonicalizeServiceScope(scope)) 442 } 443 444 serviceAccount := &compute.ServiceAccount{ 445 Email: "default", 446 Scopes: scopes, 447 } 448 449 serviceAccounts = append(serviceAccounts, serviceAccount) 450 } 451 452 // Create the instance information 453 instance := compute.Instance{ 454 CanIpForward: d.Get("can_ip_forward").(bool), 455 Description: d.Get("description").(string), 456 Disks: disks, 457 MachineType: machineType.SelfLink, 458 Metadata: resourceInstanceMetadata(d), 459 Name: d.Get("name").(string), 460 NetworkInterfaces: networkInterfaces, 461 Tags: resourceInstanceTags(d), 462 ServiceAccounts: serviceAccounts, 463 } 464 465 log.Printf("[INFO] Requesting instance creation") 466 op, err := config.clientCompute.Instances.Insert( 467 config.Project, zone.Name, &instance).Do() 468 if err != nil { 469 return fmt.Errorf("Error creating instance: %s", err) 470 } 471 472 // Store the ID now 473 d.SetId(instance.Name) 474 475 // Wait for the operation to complete 476 waitErr := resourceOperationWaitZone(config, op, zone.Name, "instance to create") 477 if waitErr != nil { 478 // The resource didn't actually create 479 d.SetId("") 480 return waitErr 481 } 482 483 return resourceComputeInstanceRead(d, meta) 484 } 485 486 func resourceComputeInstanceRead(d *schema.ResourceData, meta interface{}) error { 487 config := meta.(*Config) 488 489 instance, err := config.clientCompute.Instances.Get( 490 config.Project, d.Get("zone").(string), d.Id()).Do() 491 if err != nil { 492 if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 { 493 // The resource doesn't exist anymore 494 d.SetId("") 495 496 return nil 497 } 498 499 return fmt.Errorf("Error reading instance: %s", err) 500 } 501 502 d.Set("can_ip_forward", instance.CanIpForward) 503 504 // Set the service accounts 505 serviceAccounts := make([]map[string]interface{}, 0, 1) 506 for _, serviceAccount := range instance.ServiceAccounts { 507 scopes := make([]string, len(serviceAccount.Scopes)) 508 for i, scope := range serviceAccount.Scopes { 509 scopes[i] = scope 510 } 511 512 serviceAccounts = append(serviceAccounts, map[string]interface{}{ 513 "email": serviceAccount.Email, 514 "scopes": scopes, 515 }) 516 } 517 d.Set("service_account", serviceAccounts) 518 519 networksCount := d.Get("network.#").(int) 520 networkInterfacesCount := d.Get("network_interface.#").(int) 521 522 if networksCount > 0 && networkInterfacesCount > 0 { 523 return fmt.Errorf("Error: cannot define both networks and network_interfaces.") 524 } 525 if networksCount == 0 && networkInterfacesCount == 0 { 526 return fmt.Errorf("Error: Must define at least one network_interface.") 527 } 528 529 // Set the networks 530 // Use the first external IP found for the default connection info. 531 externalIP := "" 532 internalIP := "" 533 networks := make([]map[string]interface{}, 0, 1) 534 if networksCount > 0 { 535 // TODO: Remove this when realizing deprecation of .network 536 for i, iface := range instance.NetworkInterfaces { 537 var natIP string 538 for _, config := range iface.AccessConfigs { 539 if config.Type == "ONE_TO_ONE_NAT" { 540 natIP = config.NatIP 541 break 542 } 543 } 544 545 if externalIP == "" && natIP != "" { 546 externalIP = natIP 547 } 548 549 network := make(map[string]interface{}) 550 network["name"] = iface.Name 551 network["external_address"] = natIP 552 network["internal_address"] = iface.NetworkIP 553 network["source"] = d.Get(fmt.Sprintf("network.%d.source", i)) 554 networks = append(networks, network) 555 } 556 } 557 d.Set("network", networks) 558 559 networkInterfaces := make([]map[string]interface{}, 0, 1) 560 if networkInterfacesCount > 0 { 561 for i, iface := range instance.NetworkInterfaces { 562 // The first non-empty ip is left in natIP 563 var natIP string 564 accessConfigs := make( 565 []map[string]interface{}, 0, len(iface.AccessConfigs)) 566 for _, config := range iface.AccessConfigs { 567 accessConfigs = append(accessConfigs, map[string]interface{}{ 568 "nat_ip": config.NatIP, 569 }) 570 571 if natIP == "" { 572 natIP = config.NatIP 573 } 574 } 575 576 if externalIP == "" { 577 externalIP = natIP 578 } 579 580 if internalIP == "" { 581 internalIP = iface.NetworkIP 582 } 583 584 networkInterfaces = append(networkInterfaces, map[string]interface{}{ 585 "name": iface.Name, 586 "address": iface.NetworkIP, 587 "network": d.Get(fmt.Sprintf("network_interface.%d.network", i)), 588 "access_config": accessConfigs, 589 }) 590 } 591 } 592 d.Set("network_interface", networkInterfaces) 593 594 // Fall back on internal ip if there is no external ip. This makes sense in the situation where 595 // terraform is being used on a cloud instance and can therefore access the instances it creates 596 // via their internal ips. 597 sshIP := externalIP 598 if sshIP == "" { 599 sshIP = internalIP 600 } 601 602 // Initialize the connection info 603 d.SetConnInfo(map[string]string{ 604 "type": "ssh", 605 "host": sshIP, 606 }) 607 608 // Set the metadata fingerprint if there is one. 609 if instance.Metadata != nil { 610 d.Set("metadata_fingerprint", instance.Metadata.Fingerprint) 611 } 612 613 // Set the tags fingerprint if there is one. 614 if instance.Tags != nil { 615 d.Set("tags_fingerprint", instance.Tags.Fingerprint) 616 } 617 618 d.Set("self_link", instance.SelfLink) 619 620 return nil 621 } 622 623 func resourceComputeInstanceUpdate(d *schema.ResourceData, meta interface{}) error { 624 config := meta.(*Config) 625 626 zone := d.Get("zone").(string) 627 628 instance, err := config.clientCompute.Instances.Get( 629 config.Project, zone, d.Id()).Do() 630 if err != nil { 631 if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 { 632 // The resource doesn't exist anymore 633 d.SetId("") 634 635 return nil 636 } 637 638 return fmt.Errorf("Error reading instance: %s", err) 639 } 640 641 // Enable partial mode for the resource since it is possible 642 d.Partial(true) 643 644 // If the Metadata has changed, then update that. 645 if d.HasChange("metadata") { 646 metadata := resourceInstanceMetadata(d) 647 op, err := config.clientCompute.Instances.SetMetadata( 648 config.Project, zone, d.Id(), metadata).Do() 649 if err != nil { 650 return fmt.Errorf("Error updating metadata: %s", err) 651 } 652 653 // 1 5 2 654 opErr := resourceOperationWaitZone(config, op, zone, "metadata to update") 655 if opErr != nil { 656 return opErr 657 } 658 659 d.SetPartial("metadata") 660 } 661 662 if d.HasChange("tags") { 663 tags := resourceInstanceTags(d) 664 op, err := config.clientCompute.Instances.SetTags( 665 config.Project, zone, d.Id(), tags).Do() 666 if err != nil { 667 return fmt.Errorf("Error updating tags: %s", err) 668 } 669 670 opErr := resourceOperationWaitZone(config, op, zone, "tags to update") 671 if opErr != nil { 672 return opErr 673 } 674 675 d.SetPartial("tags") 676 } 677 678 networkInterfacesCount := d.Get("network_interface.#").(int) 679 if networkInterfacesCount > 0 { 680 // Sanity check 681 if networkInterfacesCount != len(instance.NetworkInterfaces) { 682 return fmt.Errorf("Instance had unexpected number of network interfaces: %d", len(instance.NetworkInterfaces)) 683 } 684 for i := 0; i < networkInterfacesCount; i++ { 685 prefix := fmt.Sprintf("network_interface.%d", i) 686 instNetworkInterface := instance.NetworkInterfaces[i] 687 networkName := d.Get(prefix + ".name").(string) 688 689 // TODO: This sanity check is broken by #929, disabled for now (by forcing the equality) 690 networkName = instNetworkInterface.Name 691 // Sanity check 692 if networkName != instNetworkInterface.Name { 693 return fmt.Errorf("Instance networkInterface had unexpected name: %s", instNetworkInterface.Name) 694 } 695 696 if d.HasChange(prefix + ".access_config") { 697 698 // TODO: This code deletes then recreates accessConfigs. This is bad because it may 699 // leave the machine inaccessible from either ip if the creation part fails (network 700 // timeout etc). However right now there is a GCE limit of 1 accessConfig so it is 701 // the only way to do it. In future this should be revised to only change what is 702 // necessary, and also add before removing. 703 704 // Delete any accessConfig that currently exists in instNetworkInterface 705 for _, ac := range instNetworkInterface.AccessConfigs { 706 op, err := config.clientCompute.Instances.DeleteAccessConfig( 707 config.Project, zone, d.Id(), ac.Name, networkName).Do() 708 if err != nil { 709 return fmt.Errorf("Error deleting old access_config: %s", err) 710 } 711 opErr := resourceOperationWaitZone(config, op, zone, "old access_config to delete") 712 if opErr != nil { 713 return opErr 714 } 715 } 716 717 // Create new ones 718 accessConfigsCount := d.Get(prefix + ".access_config.#").(int) 719 for j := 0; j < accessConfigsCount; j++ { 720 acPrefix := fmt.Sprintf("%s.access_config.%d", prefix, j) 721 ac := &compute.AccessConfig{ 722 Type: "ONE_TO_ONE_NAT", 723 NatIP: d.Get(acPrefix + ".nat_ip").(string), 724 } 725 op, err := config.clientCompute.Instances.AddAccessConfig( 726 config.Project, zone, d.Id(), networkName, ac).Do() 727 if err != nil { 728 return fmt.Errorf("Error adding new access_config: %s", err) 729 } 730 opErr := resourceOperationWaitZone(config, op, zone, "new access_config to add") 731 if opErr != nil { 732 return opErr 733 } 734 } 735 } 736 } 737 } 738 739 // We made it, disable partial mode 740 d.Partial(false) 741 742 return resourceComputeInstanceRead(d, meta) 743 } 744 745 func resourceComputeInstanceDelete(d *schema.ResourceData, meta interface{}) error { 746 config := meta.(*Config) 747 748 zone := d.Get("zone").(string) 749 log.Printf("[INFO] Requesting instance deletion: %s", d.Id()) 750 op, err := config.clientCompute.Instances.Delete(config.Project, zone, d.Id()).Do() 751 if err != nil { 752 return fmt.Errorf("Error deleting instance: %s", err) 753 } 754 755 // Wait for the operation to complete 756 opErr := resourceOperationWaitZone(config, op, zone, "instance to delete") 757 if opErr != nil { 758 return opErr 759 } 760 761 d.SetId("") 762 return nil 763 } 764 765 func resourceInstanceMetadata(d *schema.ResourceData) *compute.Metadata { 766 m := &compute.Metadata{} 767 if mdMap := d.Get("metadata").(map[string]interface{}); len(mdMap) > 0 { 768 m.Items = make([]*compute.MetadataItems, 0, len(mdMap)) 769 for key, val := range mdMap { 770 m.Items = append(m.Items, &compute.MetadataItems{ 771 Key: key, 772 Value: val.(string), 773 }) 774 } 775 776 // Set the fingerprint. If the metadata has never been set before 777 // then this will just be blank. 778 m.Fingerprint = d.Get("metadata_fingerprint").(string) 779 } 780 781 return m 782 } 783 784 func resourceInstanceTags(d *schema.ResourceData) *compute.Tags { 785 // Calculate the tags 786 var tags *compute.Tags 787 if v := d.Get("tags"); v != nil { 788 vs := v.(*schema.Set) 789 tags = new(compute.Tags) 790 tags.Items = make([]string, vs.Len()) 791 for i, v := range vs.List() { 792 tags.Items[i] = v.(string) 793 } 794 795 tags.Fingerprint = d.Get("tags_fingerprint").(string) 796 } 797 798 return tags 799 }