From 9851a7eaa6d5f31aec7a6e6c6739dfe05ae71879 Mon Sep 17 00:00:00 2001 From: Mike Fowler Date: Fri, 27 Jan 2017 21:06:46 +0000 Subject: [PATCH 01/24] Fix master_instance_name to prevent slave rebuilds --- resource_sql_database_instance.go | 6 ++- resource_sql_database_instance_test.go | 58 +++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/resource_sql_database_instance.go b/resource_sql_database_instance.go index 128e4b74..f07dc68f 100644 --- a/resource_sql_database_instance.go +++ b/resource_sql_database_instance.go @@ -3,6 +3,7 @@ package google import ( "fmt" "log" + "strings" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" @@ -70,6 +71,7 @@ func resourceSqlDatabaseInstance() *schema.Resource { "crash_safe_replication": &schema.Schema{ Type: schema.TypeBool, Optional: true, + Computed: true, }, "database_flags": &schema.Schema{ Type: schema.TypeList, @@ -564,7 +566,7 @@ func resourceSqlDatabaseInstanceRead(d *schema.ResourceData, meta interface{}) e _backupConfiguration["enabled"] = settings.BackupConfiguration.Enabled } - if vp, okp := _backupConfiguration["start_time"]; okp && vp != nil { + if vp, okp := _backupConfiguration["start_time"]; okp && len(vp.(string)) > 0 { _backupConfiguration["start_time"] = settings.BackupConfiguration.StartTime } @@ -758,7 +760,7 @@ func resourceSqlDatabaseInstanceRead(d *schema.ResourceData, meta interface{}) e d.Set("ip_address", _ipAddresses) if v, ok := d.GetOk("master_instance_name"); ok && v != nil { - d.Set("master_instance_name", instance.MasterInstanceName) + d.Set("master_instance_name", strings.TrimPrefix(instance.MasterInstanceName, project+":")) } d.Set("self_link", instance.SelfLink) diff --git a/resource_sql_database_instance_test.go b/resource_sql_database_instance_test.go index 15207a18..48073796 100644 --- a/resource_sql_database_instance_test.go +++ b/resource_sql_database_instance_test.go @@ -10,6 +10,7 @@ package google import ( "fmt" "strconv" + "strings" "testing" "github.com/hashicorp/terraform/helper/acctest" @@ -86,6 +87,34 @@ func TestAccGoogleSqlDatabaseInstance_settings_basic(t *testing.T) { }) } +func TestAccGoogleSqlDatabaseInstance_slave(t *testing.T) { + var instance sqladmin.DatabaseInstance + masterID := acctest.RandInt() + slaveID := acctest.RandInt() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccGoogleSqlDatabaseInstanceDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: fmt.Sprintf( + testGoogleSqlDatabaseInstance_slave, masterID, slaveID), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleSqlDatabaseInstanceExists( + "google_sql_database_instance.instance_master", &instance), + testAccCheckGoogleSqlDatabaseInstanceEquals( + "google_sql_database_instance.instance_master", &instance), + testAccCheckGoogleSqlDatabaseInstanceExists( + "google_sql_database_instance.instance_slave", &instance), + testAccCheckGoogleSqlDatabaseInstanceEquals( + "google_sql_database_instance.instance_slave", &instance), + ), + }, + }, + }) +} + func TestAccGoogleSqlDatabaseInstance_settings_upgrade(t *testing.T) { var instance sqladmin.DatabaseInstance databaseID := acctest.RandInt() @@ -199,7 +228,7 @@ func testAccCheckGoogleSqlDatabaseInstanceEquals(n string, return fmt.Errorf("Error settings.tier mismatch, (%s, %s)", server, local) } - server = instance.MasterInstanceName + server = strings.TrimPrefix(instance.MasterInstanceName, instance.Project+":") local = attributes["master_instance_name"] if server != local && len(server) > 0 && len(local) > 0 { return fmt.Errorf("Error master_instance_name mismatch, (%s, %s)", server, local) @@ -474,6 +503,33 @@ resource "google_sql_database_instance" "instance" { } ` +var testGoogleSqlDatabaseInstance_slave = ` +resource "google_sql_database_instance" "instance_master" { + name = "tf-lw-%d" + region = "us-central1" + + settings { + tier = "db-f1-micro" + + backup_configuration { + enabled = true + binary_log_enabled = true + } + } +} + +resource "google_sql_database_instance" "instance_slave" { + name = "tf-lw-%d" + region = "us-central1" + + master_instance_name = "${google_sql_database_instance.instance_master.name}" + + settings { + tier = "db-f1-micro" + } +} +` + var testGoogleSqlDatabaseInstance_authNets_step1 = ` resource "google_sql_database_instance" "instance" { name = "tf-lw-%d" From c058cdcbed0c6dda2f4671ac9394c1e2a2ea8716 Mon Sep 17 00:00:00 2001 From: Roberto Jung Drebes Date: Wed, 4 Jan 2017 10:35:44 +0100 Subject: [PATCH 02/24] provider/google: remote_traffic_selector for google_compute_vpn_tunnel --- resource_compute_vpn_tunnel.go | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/resource_compute_vpn_tunnel.go b/resource_compute_vpn_tunnel.go index 989764c2..7f78688c 100644 --- a/resource_compute_vpn_tunnel.go +++ b/resource_compute_vpn_tunnel.go @@ -72,6 +72,14 @@ func resourceComputeVpnTunnel() *schema.Resource { Set: schema.HashString, }, + "remote_traffic_selector": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "project": &schema.Schema{ Type: schema.TypeString, Optional: true, @@ -124,15 +132,24 @@ func resourceComputeVpnTunnelCreate(d *schema.ResourceData, meta interface{}) er } } + var remoteTrafficSelectors []string + if v := d.Get("remote_traffic_selector").(*schema.Set); v.Len() > 0 { + remoteTrafficSelectors = make([]string, v.Len()) + for i, v := range v.List() { + remoteTrafficSelectors[i] = v.(string) + } + } + vpnTunnelsService := compute.NewVpnTunnelsService(config.clientCompute) vpnTunnel := &compute.VpnTunnel{ - Name: name, - PeerIp: peerIp, - SharedSecret: sharedSecret, - TargetVpnGateway: targetVpnGateway, - IkeVersion: int64(ikeVersion), - LocalTrafficSelector: localTrafficSelectors, + Name: name, + PeerIp: peerIp, + SharedSecret: sharedSecret, + TargetVpnGateway: targetVpnGateway, + IkeVersion: int64(ikeVersion), + LocalTrafficSelector: localTrafficSelectors, + RemoteTrafficSelector: remoteTrafficSelectors, } if v, ok := d.GetOk("description"); ok { From 515b12f7509ed676c03c701f2aa7d38ddf2500f4 Mon Sep 17 00:00:00 2001 From: Roberto Jung Drebes Date: Sat, 28 Jan 2017 00:43:45 +0100 Subject: [PATCH 03/24] provider/google: acceptance tests for traffic selectors --- resource_compute_vpn_tunnel.go | 12 ++++++++++++ resource_compute_vpn_tunnel_test.go | 19 +++++++++++++++---- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/resource_compute_vpn_tunnel.go b/resource_compute_vpn_tunnel.go index 7f78688c..7989035d 100644 --- a/resource_compute_vpn_tunnel.go +++ b/resource_compute_vpn_tunnel.go @@ -199,6 +199,18 @@ func resourceComputeVpnTunnelRead(d *schema.ResourceData, meta interface{}) erro return fmt.Errorf("Error Reading VPN Tunnel %s: %s", name, err) } + localTrafficSelectors := []string{} + for _, lts := range vpnTunnel.LocalTrafficSelector { + localTrafficSelectors = append(localTrafficSelectors, lts) + } + d.Set("local_traffic_selector", localTrafficSelectors) + + remoteTrafficSelectors := []string{} + for _, rts := range vpnTunnel.RemoteTrafficSelector { + remoteTrafficSelectors = append(remoteTrafficSelectors, rts) + } + d.Set("remote_traffic_selector", remoteTrafficSelectors) + d.Set("detailed_status", vpnTunnel.DetailedStatus) d.Set("self_link", vpnTunnel.SelfLink) diff --git a/resource_compute_vpn_tunnel_test.go b/resource_compute_vpn_tunnel_test.go index 896c94c4..c863fce6 100644 --- a/resource_compute_vpn_tunnel_test.go +++ b/resource_compute_vpn_tunnel_test.go @@ -22,6 +22,10 @@ func TestAccComputeVpnTunnel_basic(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckComputeVpnTunnelExists( "google_compute_vpn_tunnel.foobar"), + resource.TestCheckResourceAttr( + "google_compute_vpn_tunnel.foobar", "local_traffic_selector.#", "1"), + resource.TestCheckResourceAttr( + "google_compute_vpn_tunnel.foobar", "remote_traffic_selector.#", "2"), ), }, }, @@ -83,16 +87,21 @@ func testAccCheckComputeVpnTunnelExists(n string) resource.TestCheckFunc { var testAccComputeVpnTunnel_basic = fmt.Sprintf(` resource "google_compute_network" "foobar" { name = "tunnel-test-%s" - ipv4_range = "10.0.0.0/16" +} +resource "google_compute_subnetwork" "foobar" { + name = "tunnel-test-%s" + network = "${google_compute_network.foobar.self_link}" + ip_cidr_range = "10.0.0.0/16" + region = "us-central1" } resource "google_compute_address" "foobar" { name = "tunnel-test-%s" - region = "us-central1" + region = "${google_compute_subnetwork.foobar.region}" } resource "google_compute_vpn_gateway" "foobar" { name = "tunnel-test-%s" network = "${google_compute_network.foobar.self_link}" - region = "${google_compute_address.foobar.region}" + region = "${google_compute_subnetwork.foobar.region}" } resource "google_compute_forwarding_rule" "foobar_esp" { name = "tunnel-test-%s" @@ -123,6 +132,8 @@ resource "google_compute_vpn_tunnel" "foobar" { target_vpn_gateway = "${google_compute_vpn_gateway.foobar.self_link}" shared_secret = "unguessable" peer_ip = "8.8.8.8" + local_traffic_selector = ["${google_compute_subnetwork.foobar.ip_cidr_range}"] + remote_traffic_selector = ["192.168.0.0/24", "192.168.1.0/24"] }`, acctest.RandString(10), acctest.RandString(10), acctest.RandString(10), acctest.RandString(10), acctest.RandString(10), acctest.RandString(10), - acctest.RandString(10)) + acctest.RandString(10), acctest.RandString(10)) From ef01f8259154529e425bae8cb6969e64e6bf3d1e Mon Sep 17 00:00:00 2001 From: Christoph Blecker Date: Sun, 29 Jan 2017 07:59:55 -0800 Subject: [PATCH 04/24] Fix vet errors in providers (#11496) --- resource_google_project_iam_policy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resource_google_project_iam_policy.go b/resource_google_project_iam_policy.go index 00890bb6..cf9c87ef 100644 --- a/resource_google_project_iam_policy.go +++ b/resource_google_project_iam_policy.go @@ -257,7 +257,7 @@ func setProjectIamPolicy(policy *cloudresourcemanager.Policy, config *Config, pi &cloudresourcemanager.SetIamPolicyRequest{Policy: policy}).Do() if err != nil { - return fmt.Errorf("Error applying IAM policy for project %q. Policy is %+s, error is %s", pid, policy, err) + return fmt.Errorf("Error applying IAM policy for project %q. Policy is %#v, error is %s", pid, policy, err) } return nil } From 4950e66d688ce725e0fb41ed57a4bb731e8a7494 Mon Sep 17 00:00:00 2001 From: Dana Hoffman Date: Mon, 30 Jan 2017 03:35:35 -0800 Subject: [PATCH 05/24] provider/google: allow instance group managers in region other than project (#11294) --- resource_compute_instance_group_manager.go | 26 +++++-- ...rce_compute_instance_group_manager_test.go | 70 +++++++++++++++++++ 2 files changed, 91 insertions(+), 5 deletions(-) diff --git a/resource_compute_instance_group_manager.go b/resource_compute_instance_group_manager.go index 89bff60d..56d1e7ee 100644 --- a/resource_compute_instance_group_manager.go +++ b/resource_compute_instance_group_manager.go @@ -216,17 +216,33 @@ func resourceComputeInstanceGroupManagerRead(d *schema.ResourceData, meta interf return config.clientCompute.InstanceGroupManagers.Get(project, zone, d.Id()).Do() } - resource, err := getZonalResourceFromRegion(getInstanceGroupManager, region, config.clientCompute, project) - if err != nil { - return err + var manager *compute.InstanceGroupManager + var e error + if zone, ok := d.GetOk("zone"); ok { + manager, e = config.clientCompute.InstanceGroupManagers.Get(project, zone.(string), d.Id()).Do() + + if e != nil { + return e + } + } else { + // If the resource was imported, the only info we have is the ID. Try to find the resource + // by searching in the region of the project. + var resource interface{} + resource, e = getZonalResourceFromRegion(getInstanceGroupManager, region, config.clientCompute, project) + + if e != nil { + return e + } + + manager = resource.(*compute.InstanceGroupManager) } - if resource == nil { + + if manager == nil { log.Printf("[WARN] Removing Instance Group Manager %q because it's gone", d.Get("name").(string)) // The resource doesn't exist anymore d.SetId("") return nil } - manager := resource.(*compute.InstanceGroupManager) zoneUrl := strings.Split(manager.Zone, "/") d.Set("base_instance_name", manager.BaseInstanceName) diff --git a/resource_compute_instance_group_manager_test.go b/resource_compute_instance_group_manager_test.go index a16646db..22e35d16 100644 --- a/resource_compute_instance_group_manager_test.go +++ b/resource_compute_instance_group_manager_test.go @@ -135,6 +135,30 @@ func TestAccInstanceGroupManager_updateStrategy(t *testing.T) { }) } +func TestAccInstanceGroupManager_separateRegions(t *testing.T) { + var manager compute.InstanceGroupManager + + igm1 := fmt.Sprintf("igm-test-%s", acctest.RandString(10)) + igm2 := fmt.Sprintf("igm-test-%s", acctest.RandString(10)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckInstanceGroupManagerDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccInstanceGroupManager_separateRegions(igm1, igm2), + Check: resource.ComposeTestCheckFunc( + testAccCheckInstanceGroupManagerExists( + "google_compute_instance_group_manager.igm-basic", &manager), + testAccCheckInstanceGroupManagerExists( + "google_compute_instance_group_manager.igm-basic-2", &manager), + ), + }, + }, + }) +} + func testAccCheckInstanceGroupManagerDestroy(s *terraform.State) error { config := testAccProvider.Meta().(*Config) @@ -571,6 +595,52 @@ func testAccInstanceGroupManager_updateStrategy(igm string) string { }`, igm) } +func testAccInstanceGroupManager_separateRegions(igm1, igm2 string) string { + return fmt.Sprintf(` + resource "google_compute_instance_template" "igm-basic" { + machine_type = "n1-standard-1" + can_ip_forward = false + tags = ["foo", "bar"] + + disk { + source_image = "debian-cloud/debian-8-jessie-v20160803" + auto_delete = true + boot = true + } + + network_interface { + network = "default" + } + + metadata { + foo = "bar" + } + + service_account { + scopes = ["userinfo-email", "compute-ro", "storage-ro"] + } + } + + resource "google_compute_instance_group_manager" "igm-basic" { + description = "Terraform test instance group manager" + name = "%s" + instance_template = "${google_compute_instance_template.igm-basic.self_link}" + base_instance_name = "igm-basic" + zone = "us-central1-c" + target_size = 2 + } + + resource "google_compute_instance_group_manager" "igm-basic-2" { + description = "Terraform test instance group manager" + name = "%s" + instance_template = "${google_compute_instance_template.igm-basic.self_link}" + base_instance_name = "igm-basic-2" + zone = "us-west1-b" + target_size = 2 + } + `, igm1, igm2) +} + func resourceSplitter(resource string) string { splits := strings.Split(resource, "/") From 5b551718d51d3491c39cb7568999b7a772b08006 Mon Sep 17 00:00:00 2001 From: Christophe van de Kerchove Date: Tue, 31 Jan 2017 09:33:29 -0500 Subject: [PATCH 06/24] This should resolve #11547 This should force terraform to recreate the resource after updating it. --- resource_compute_instance_template.go | 1 + 1 file changed, 1 insertion(+) diff --git a/resource_compute_instance_template.go b/resource_compute_instance_template.go index 9b9798dc..fa0f5b6b 100644 --- a/resource_compute_instance_template.go +++ b/resource_compute_instance_template.go @@ -212,6 +212,7 @@ func resourceComputeInstanceTemplate() *schema.Resource { "access_config": &schema.Schema{ Type: schema.TypeList, Optional: true, + ForceNew: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "nat_ip": &schema.Schema{ From a6c1f944195dcbc00439719e02d5e181045a74ca Mon Sep 17 00:00:00 2001 From: Evan Brown Date: Tue, 31 Jan 2017 22:21:11 -0800 Subject: [PATCH 07/24] providers/google: No default root user for SQL Cloud SQL Gen 2 instances come with a default 'root'@'%' user on creation. This change automatically deletes that user after creation. A Terraform user must use the google_sql_user to create a user with appropriate host and password. --- resource_sql_database_instance.go | 25 +++++++++++- resource_sql_database_instance_test.go | 54 ++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/resource_sql_database_instance.go b/resource_sql_database_instance.go index f07dc68f..8a7b25b4 100644 --- a/resource_sql_database_instance.go +++ b/resource_sql_database_instance.go @@ -502,7 +502,30 @@ func resourceSqlDatabaseInstanceCreate(d *schema.ResourceData, meta interface{}) return err } - return resourceSqlDatabaseInstanceRead(d, meta) + err = resourceSqlDatabaseInstanceRead(d, meta) + if err != nil { + return err + } + + // If a root user exists with a wildcard ('%') hostname, delete it. + users, err := config.clientSqlAdmin.Users.List(project, instance.Name).Do() + if err != nil { + return fmt.Errorf("Error, attempting to list users associated with instance %s: %s", instance.Name, err) + } + for _, u := range users.Items { + if u.Name == "root" && u.Host == "%" { + op, err = config.clientSqlAdmin.Users.Delete(project, instance.Name, u.Host, u.Name).Do() + if err != nil { + return fmt.Errorf("Error, failed to delete default 'root'@'*' user, but the database was created successfully: %s", err) + } + err = sqladminOperationWait(config, op, "Delete default root User") + if err != nil { + return err + } + } + } + + return nil } func resourceSqlDatabaseInstanceRead(d *schema.ResourceData, meta interface{}) error { diff --git a/resource_sql_database_instance_test.go b/resource_sql_database_instance_test.go index 48073796..e36cfcd2 100644 --- a/resource_sql_database_instance_test.go +++ b/resource_sql_database_instance_test.go @@ -64,6 +64,30 @@ func TestAccGoogleSqlDatabaseInstance_basic2(t *testing.T) { }) } +func TestAccGoogleSqlDatabaseInstance_basic3(t *testing.T) { + var instance sqladmin.DatabaseInstance + databaseID := acctest.RandInt() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccGoogleSqlDatabaseInstanceDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: fmt.Sprintf( + testGoogleSqlDatabaseInstance_basic3, databaseID), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleSqlDatabaseInstanceExists( + "google_sql_database_instance.instance", &instance), + testAccCheckGoogleSqlDatabaseInstanceEquals( + "google_sql_database_instance.instance", &instance), + testAccCheckGoogleSqlDatabaseRootUserDoesNotExist( + &instance), + ), + }, + }, + }) +} func TestAccGoogleSqlDatabaseInstance_settings_basic(t *testing.T) { var instance sqladmin.DatabaseInstance databaseID := acctest.RandInt() @@ -406,6 +430,27 @@ func testAccGoogleSqlDatabaseInstanceDestroy(s *terraform.State) error { return nil } +func testAccCheckGoogleSqlDatabaseRootUserDoesNotExist( + instance *sqladmin.DatabaseInstance) resource.TestCheckFunc { + return func(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + + users, err := config.clientSqlAdmin.Users.List(config.Project, instance.Name).Do() + + if err != nil { + return fmt.Errorf("Could not list database users for %q: %s", instance.Name, err) + } + + for _, u := range users.Items { + if u.Name == "root" && u.Host == "%" { + return fmt.Errorf("%v@%v user still exists", u.Name, u.Host) + } + } + + return nil + } +} + var testGoogleSqlDatabaseInstance_basic = ` resource "google_sql_database_instance" "instance" { name = "tf-lw-%d" @@ -426,6 +471,15 @@ resource "google_sql_database_instance" "instance" { } } ` +var testGoogleSqlDatabaseInstance_basic3 = ` +resource "google_sql_database_instance" "instance" { + name = "tf-lw-%d" + region = "us-central" + settings { + tier = "db-f1-micro" + } +} +` var testGoogleSqlDatabaseInstance_settings = ` resource "google_sql_database_instance" "instance" { From 917aa65405de757d2b9cd9c925a0760a5e8cee7d Mon Sep 17 00:00:00 2001 From: Mike Fowler Date: Wed, 1 Feb 2017 16:20:31 +0000 Subject: [PATCH 08/24] provider/google-cloud: Add second generation disk specification options (#11571) * Add second generation disk specification options. * Adjust test check to match resource read behaviour. --- resource_sql_database_instance.go | 56 ++++++++++++++++++++++++++ resource_sql_database_instance_test.go | 55 +++++++++++++++++++++++++ 2 files changed, 111 insertions(+) diff --git a/resource_sql_database_instance.go b/resource_sql_database_instance.go index f07dc68f..a51044ba 100644 --- a/resource_sql_database_instance.go +++ b/resource_sql_database_instance.go @@ -89,6 +89,18 @@ func resourceSqlDatabaseInstance() *schema.Resource { }, }, }, + "disk_autoresize": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + "disk_size": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + }, + "disk_type": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, "ip_configuration": &schema.Schema{ Type: schema.TypeList, Optional: true, @@ -325,6 +337,18 @@ func resourceSqlDatabaseInstanceCreate(d *schema.ResourceData, meta interface{}) settings.CrashSafeReplicationEnabled = v.(bool) } + if v, ok := _settings["disk_autoresize"]; ok && v.(bool) { + settings.StorageAutoResize = v.(bool) + } + + if v, ok := _settings["disk_size"]; ok && v.(int) > 0 { + settings.DataDiskSizeGb = int64(v.(int)) + } + + if v, ok := _settings["disk_type"]; ok && len(v.(string)) > 0 { + settings.DataDiskType = v.(string) + } + if v, ok := _settings["database_flags"]; ok { settings.DatabaseFlags = make([]*sqladmin.DatabaseFlags, 0) _databaseFlagsList := v.([]interface{}) @@ -579,6 +603,24 @@ func resourceSqlDatabaseInstanceRead(d *schema.ResourceData, meta interface{}) e _settings["crash_safe_replication"] = settings.CrashSafeReplicationEnabled } + if v, ok := _settings["disk_autoresize"]; ok && v != nil { + if v.(bool) { + _settings["disk_autoresize"] = settings.StorageAutoResize + } + } + + if v, ok := _settings["disk_size"]; ok && v != nil { + if v.(int) > 0 && settings.DataDiskSizeGb < int64(v.(int)) { + _settings["disk_size"] = settings.DataDiskSizeGb + } + } + + if v, ok := _settings["disk_type"]; ok && v != nil { + if len(v.(string)) > 0 { + _settings["disk_type"] = settings.DataDiskType + } + } + if v, ok := _settings["database_flags"]; ok && len(v.([]interface{})) > 0 { _flag_map := make(map[string]string) // First keep track of localy defined flag pairs @@ -842,6 +884,20 @@ func resourceSqlDatabaseInstanceUpdate(d *schema.ResourceData, meta interface{}) settings.CrashSafeReplicationEnabled = v.(bool) } + if v, ok := _settings["disk_autoresize"]; ok && v.(bool) { + settings.StorageAutoResize = v.(bool) + } + + if v, ok := _settings["disk_size"]; ok { + if v.(int) > 0 && int64(v.(int)) > instance.Settings.DataDiskSizeGb { + settings.DataDiskSizeGb = int64(v.(int)) + } + } + + if v, ok := _settings["disk_type"]; ok && len(v.(string)) > 0 { + settings.DataDiskType = v.(string) + } + _oldDatabaseFlags := make([]interface{}, 0) if ov, ook := _o["database_flags"]; ook { _oldDatabaseFlags = ov.([]interface{}) diff --git a/resource_sql_database_instance_test.go b/resource_sql_database_instance_test.go index 48073796..ac225379 100644 --- a/resource_sql_database_instance_test.go +++ b/resource_sql_database_instance_test.go @@ -115,6 +115,29 @@ func TestAccGoogleSqlDatabaseInstance_slave(t *testing.T) { }) } +func TestAccGoogleSqlDatabaseInstance_diskspecs(t *testing.T) { + var instance sqladmin.DatabaseInstance + masterID := acctest.RandInt() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccGoogleSqlDatabaseInstanceDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: fmt.Sprintf( + testGoogleSqlDatabaseInstance_diskspecs, masterID), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleSqlDatabaseInstanceExists( + "google_sql_database_instance.instance", &instance), + testAccCheckGoogleSqlDatabaseInstanceEquals( + "google_sql_database_instance.instance", &instance), + ), + }, + }, + }) +} + func TestAccGoogleSqlDatabaseInstance_settings_upgrade(t *testing.T) { var instance sqladmin.DatabaseInstance databaseID := acctest.RandInt() @@ -266,6 +289,24 @@ func testAccCheckGoogleSqlDatabaseInstanceEquals(n string, return fmt.Errorf("Error settings.crash_safe_replication mismatch, (%s, %s)", server, local) } + server = strconv.FormatBool(instance.Settings.StorageAutoResize) + local = attributes["settings.0.disk_autoresize"] + if server != local && len(server) > 0 && len(local) > 0 { + return fmt.Errorf("Error settings.disk_autoresize mismatch, (%s, %s)", server, local) + } + + server = strconv.FormatInt(instance.Settings.DataDiskSizeGb, 10) + local = attributes["settings.0.disk_size"] + if server != local && len(server) > 0 && len(local) > 0 && local != "0" { + return fmt.Errorf("Error settings.disk_size mismatch, (%s, %s)", server, local) + } + + server = instance.Settings.DataDiskType + local = attributes["settings.0.disk_type"] + if server != local && len(server) > 0 && len(local) > 0 { + return fmt.Errorf("Error settings.disk_type mismatch, (%s, %s)", server, local) + } + if instance.Settings.IpConfiguration != nil { server = strconv.FormatBool(instance.Settings.IpConfiguration.Ipv4Enabled) local = attributes["settings.0.ip_configuration.0.ipv4_enabled"] @@ -530,6 +571,20 @@ resource "google_sql_database_instance" "instance_slave" { } ` +var testGoogleSqlDatabaseInstance_diskspecs = ` +resource "google_sql_database_instance" "instance" { + name = "tf-lw-%d" + region = "us-central1" + + settings { + tier = "db-f1-micro" + disk_autoresize = true + disk_size = 15 + disk_type = "PD_HDD" + } +} +` + var testGoogleSqlDatabaseInstance_authNets_step1 = ` resource "google_sql_database_instance" "instance" { name = "tf-lw-%d" From b6f8934b2cc97c01b52fd6d60282284d8e686915 Mon Sep 17 00:00:00 2001 From: Dana Hoffman Date: Thu, 2 Feb 2017 17:37:03 -0800 Subject: [PATCH 09/24] provider/google: only set additional zones on read if it had been set in the config --- resource_container_cluster.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/resource_container_cluster.go b/resource_container_cluster.go index 19ab48a9..07e679c1 100644 --- a/resource_container_cluster.go +++ b/resource_container_cluster.go @@ -444,7 +444,10 @@ func resourceContainerClusterRead(d *schema.ResourceData, meta interface{}) erro d.Set("name", cluster.Name) d.Set("zone", cluster.Zone) - d.Set("additional_zones", cluster.Locations) + + if _, ok := d.GetOk("additional_zones"); ok { + d.Set("additional_zones", cluster.Locations) + } d.Set("endpoint", cluster.Endpoint) masterAuth := []map[string]interface{}{ From ef398e21300a3e75c32ce1f9958df711a457e11c Mon Sep 17 00:00:00 2001 From: Matthew Frahry Date: Fri, 3 Feb 2017 04:50:57 -0700 Subject: [PATCH 10/24] Add test to PR 9320 (#11645) --- resource_compute_instance.go | 4 +++ resource_compute_instance_test.go | 60 +++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/resource_compute_instance.go b/resource_compute_instance.go index c25cd87c..1c7bd021 100644 --- a/resource_compute_instance.go +++ b/resource_compute_instance.go @@ -671,6 +671,10 @@ func resourceComputeInstanceRead(d *schema.ResourceData, meta interface{}) error d.Set("can_ip_forward", instance.CanIpForward) + machineTypeResource := strings.Split(instance.MachineType, "/") + machineType := machineTypeResource[len(machineTypeResource)-1] + d.Set("machine_type", machineType) + // Set the service accounts serviceAccounts := make([]map[string]interface{}, 0, 1) for _, serviceAccount := range instance.ServiceAccounts { diff --git a/resource_compute_instance_test.go b/resource_compute_instance_test.go index 382e5c71..a4d52d87 100644 --- a/resource_compute_instance_test.go +++ b/resource_compute_instance_test.go @@ -547,6 +547,66 @@ func TestAccComputeInstance_invalid_disk(t *testing.T) { }) } +func TestAccComputeInstance_forceChangeMachineTypeManually(t *testing.T) { + var instance compute.Instance + var instanceName = fmt.Sprintf("instance-test-%s", acctest.RandString(10)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeInstanceDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeInstance_basic(instanceName), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists("google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceUpdateMachineType("google_compute_instance.foobar"), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckComputeInstanceUpdateMachineType(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + config := testAccProvider.Meta().(*Config) + + op, err := config.clientCompute.Instances.Stop(config.Project, rs.Primary.Attributes["zone"], rs.Primary.ID).Do() + if err != nil { + return fmt.Errorf("Could not stop instance: %s", err) + } + err = computeOperationWaitZone(config, op, config.Project, rs.Primary.Attributes["zone"], "Waiting on stop") + if err != nil { + return fmt.Errorf("Could not stop instance: %s", err) + } + + machineType := compute.InstancesSetMachineTypeRequest{ + MachineType: "zones/us-central1-a/machineTypes/f1-micro", + } + + op, err = config.clientCompute.Instances.SetMachineType( + config.Project, rs.Primary.Attributes["zone"], rs.Primary.ID, &machineType).Do() + if err != nil { + return fmt.Errorf("Could not change machine type: %s", err) + } + err = computeOperationWaitZone(config, op, config.Project, rs.Primary.Attributes["zone"], "Waiting machine type change") + if err != nil { + return fmt.Errorf("Could not change machine type: %s", err) + } + return nil + } +} + func testAccCheckComputeInstanceDestroy(s *terraform.State) error { config := testAccProvider.Meta().(*Config) From 4d2b136a12573f98797d787ef0241294423bff71 Mon Sep 17 00:00:00 2001 From: Evan Brown Date: Mon, 6 Feb 2017 14:16:22 -0800 Subject: [PATCH 11/24] providers/google: Fix google_project IAM bug This changes removes read of the deprecated `policy_data` attr in the `google_project` resource. 0.8.5 introduced new behavior that incorrectly read the `policy_data` field during the read lifecycle event. This caused Terraform to assume it owned not just policy defined in the data source, but everything that was associated with the project. Migrating from 0.8.4 to 0.8.5, this would cause the config (partial) to be compared to the state (complete, as it was read from the API) and assume some policies had been explicitly deleted. Terraform would then delete them. Fixes #11556 --- resource_google_project.go | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/resource_google_project.go b/resource_google_project.go index 4bc26c45..24dc56b8 100644 --- a/resource_google_project.go +++ b/resource_google_project.go @@ -196,20 +196,6 @@ func resourceGoogleProjectRead(d *schema.ResourceData, meta interface{}) error { d.Set("org_id", p.Parent.Id) } - // Read the IAM policy - pol, err := getProjectIamPolicy(pid, config) - if err != nil { - return err - } - - polBytes, err := json.Marshal(pol) - if err != nil { - return err - } - - d.Set("policy_etag", pol.Etag) - d.Set("policy_data", string(polBytes)) - return nil } From 6dfca0f836b04053f03781943c6235ece2609295 Mon Sep 17 00:00:00 2001 From: Paddy Date: Mon, 6 Feb 2017 22:09:53 -0800 Subject: [PATCH 12/24] Add a test that would have caught backwards incompatibility. Add a test that would have caught the backwards incompatibility where project IAM bindings aren't merged, but are overwritten. --- resource_google_project_test.go | 69 +++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/resource_google_project_test.go b/resource_google_project_test.go index aa3c03c5..03bdeee0 100644 --- a/resource_google_project_test.go +++ b/resource_google_project_test.go @@ -48,6 +48,34 @@ func TestAccGoogleProject_create(t *testing.T) { }) } +// Test that a Project resource merges the IAM policies that already +// exist, and won't lock people out. +func TestAccGoogleProject_merge(t *testing.T) { + pid := "terraform-" + acctest.RandString(10) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + // when policy_data is set, merge + { + Config: testAccGoogleProject_toMerge(pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleProjectExists("google_project.acceptance", pid), + testAccCheckGoogleProjectHasMoreBindingsThan(pid, 1), + ), + }, + // when policy_data is unset, restore to what it was + { + Config: testAccGoogleProject_mergeEmpty(pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleProjectExists("google_project.acceptance", pid), + testAccCheckGoogleProjectHasMoreBindingsThan(pid, 0), + ), + }, + }, + }) +} + func testAccCheckGoogleProjectExists(r, pid string) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[r] @@ -67,6 +95,19 @@ func testAccCheckGoogleProjectExists(r, pid string) resource.TestCheckFunc { } } +func testAccCheckGoogleProjectHasMoreBindingsThan(pid string, count int) resource.TestCheckFunc { + return func(s *terraform.State) error { + policy, err := getProjectIamPolicy(pid, testAccProvider.Meta().(*Config)) + if err != nil { + return err + } + if len(policy.Bindings) <= count { + return fmt.Errorf("Expected more than %d bindings, got %d: %#v", count, len(policy.Bindings), policy.Bindings) + } + return nil + } +} + func testAccGoogleProjectImportExisting(pid string) string { return fmt.Sprintf(` resource "google_project" "acceptance" { @@ -98,3 +139,31 @@ data "google_iam_policy" "admin" { } }`, pid) } + +func testAccGoogleProject_toMerge(pid, name, org string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + name = "%s" + org_id = "%s" + policy_data = "${data.google_iam_policy.acceptance.policy_data}" +} + +data "google_iam_policy" "acceptance" { + binding { + role = "roles/storage.objectViewer" + members = [ + "user:evanbrown@google.com", + ] + } +}`, pid, name, org) +} + +func testAccGoogleProject_mergeEmpty(pid, name, org string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + name = "%s" + org_id = "%s" +}`, pid, name, org) +} From e0663f35b2d2d8a9d22a42dd2e4d910ec95f82c7 Mon Sep 17 00:00:00 2001 From: Dana Hoffman Date: Mon, 6 Feb 2017 17:21:34 -0800 Subject: [PATCH 13/24] providers/google: disallow specifying the original zone in additional_zones, change field to computed --- resource_container_cluster.go | 20 ++++++++++++-------- resource_container_cluster_test.go | 16 ++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/resource_container_cluster.go b/resource_container_cluster.go index 07e679c1..a61149d0 100644 --- a/resource_container_cluster.go +++ b/resource_container_cluster.go @@ -95,6 +95,7 @@ func resourceContainerCluster() *schema.Resource { "additional_zones": &schema.Schema{ Type: schema.TypeList, Optional: true, + Computed: true, ForceNew: true, Elem: &schema.Schema{Type: schema.TypeString}, }, @@ -292,18 +293,14 @@ func resourceContainerClusterCreate(d *schema.ResourceData, meta interface{}) er if v, ok := d.GetOk("additional_zones"); ok { locationsList := v.([]interface{}) locations := []string{} - zoneInLocations := false for _, v := range locationsList { location := v.(string) locations = append(locations, location) if location == zoneName { - zoneInLocations = true + return fmt.Errorf("additional_zones should not contain the original 'zone'.") } } - if !zoneInLocations { - // zone must be in locations if specified separately - locations = append(locations, zoneName) - } + locations = append(locations, zoneName) cluster.Locations = locations } @@ -445,9 +442,16 @@ func resourceContainerClusterRead(d *schema.ResourceData, meta interface{}) erro d.Set("name", cluster.Name) d.Set("zone", cluster.Zone) - if _, ok := d.GetOk("additional_zones"); ok { - d.Set("additional_zones", cluster.Locations) + if len(cluster.Locations) > 1 { + locations := []string{} + for _, location := range cluster.Locations { + if location != cluster.Zone { + locations = append(locations, location) + } + } + d.Set("additional_zones", locations) } + d.Set("endpoint", cluster.Endpoint) masterAuth := []map[string]interface{}{ diff --git a/resource_container_cluster_test.go b/resource_container_cluster_test.go index 364de87e..6359ab42 100644 --- a/resource_container_cluster_test.go +++ b/resource_container_cluster_test.go @@ -39,7 +39,7 @@ func TestAccContainerCluster_withAdditionalZones(t *testing.T) { testAccCheckContainerClusterExists( "google_container_cluster.with_additional_zones"), testAccCheckContainerClusterAdditionalZonesExist( - "google_container_cluster.with_additional_zones"), + "google_container_cluster.with_additional_zones", 2), ), }, }, @@ -163,23 +163,19 @@ func testAccCheckContainerClusterExists(n string) resource.TestCheckFunc { } } -func testAccCheckContainerClusterAdditionalZonesExist(n string) resource.TestCheckFunc { +func testAccCheckContainerClusterAdditionalZonesExist(n string, num int) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] if !ok { return fmt.Errorf("Not found: %s", n) } - var ( - additionalZonesSize int - err error - ) - - if additionalZonesSize, err = strconv.Atoi(rs.Primary.Attributes["additional_zones.#"]); err != nil { + additionalZonesSize, err := strconv.Atoi(rs.Primary.Attributes["additional_zones.#"]) + if err != nil { return err } - if additionalZonesSize != 2 { - return fmt.Errorf("number of additional zones did not match 2") + if additionalZonesSize != num { + return fmt.Errorf("number of additional zones did not match %d, was %d", num, additionalZonesSize) } return nil From 562c25dba83611cac4c5131bd3deede614ee3814 Mon Sep 17 00:00:00 2001 From: Dana Hoffman Date: Tue, 7 Feb 2017 19:21:00 -0800 Subject: [PATCH 14/24] provider/google: always set additional_zones on read --- resource_container_cluster.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resource_container_cluster.go b/resource_container_cluster.go index a61149d0..fd9aa43a 100644 --- a/resource_container_cluster.go +++ b/resource_container_cluster.go @@ -442,15 +442,15 @@ func resourceContainerClusterRead(d *schema.ResourceData, meta interface{}) erro d.Set("name", cluster.Name) d.Set("zone", cluster.Zone) + locations := []string{} if len(cluster.Locations) > 1 { - locations := []string{} for _, location := range cluster.Locations { if location != cluster.Zone { locations = append(locations, location) } } - d.Set("additional_zones", locations) } + d.Set("additional_zones", locations) d.Set("endpoint", cluster.Endpoint) From 4094ed6dc9b547c05d2595124b9798d39733bf7a Mon Sep 17 00:00:00 2001 From: Paddy Date: Wed, 8 Feb 2017 10:24:04 -0800 Subject: [PATCH 15/24] provider/google: update DNS names in tests. Our DNS tests were using terraform.test as a DNS name, which GCP was erroring on, as we haven't proven we own the domain (and can't, as we don't). To solve this, I updated the tests to use hashicorptest.com, which we _do_ own, and which we have proven ownership of. The tests now pass. --- resource_dns_managed_zone_test.go | 2 +- resource_dns_record_set_test.go | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/resource_dns_managed_zone_test.go b/resource_dns_managed_zone_test.go index c136c8e1..73d55128 100644 --- a/resource_dns_managed_zone_test.go +++ b/resource_dns_managed_zone_test.go @@ -79,5 +79,5 @@ func testAccCheckDnsManagedZoneExists(n string, zone *dns.ManagedZone) resource. var testAccDnsManagedZone_basic = fmt.Sprintf(` resource "google_dns_managed_zone" "foobar" { name = "mzone-test-%s" - dns_name = "terraform.test." + dns_name = "hashicorptest.com." }`, acctest.RandString(10)) diff --git a/resource_dns_record_set_test.go b/resource_dns_record_set_test.go index 1a128b7d..35e1ac34 100644 --- a/resource_dns_record_set_test.go +++ b/resource_dns_record_set_test.go @@ -138,12 +138,12 @@ func testAccDnsRecordSet_basic(zoneName string, addr2 string, ttl int) string { return fmt.Sprintf(` resource "google_dns_managed_zone" "parent-zone" { name = "%s" - dns_name = "terraform.test." + dns_name = "hashicorptest.com." description = "Test Description" } resource "google_dns_record_set" "foobar" { managed_zone = "${google_dns_managed_zone.parent-zone.name}" - name = "test-record.terraform.test." + name = "test-record.hashicorptest.com." type = "A" rrdatas = ["127.0.0.1", "%s"] ttl = %d @@ -155,12 +155,12 @@ func testAccDnsRecordSet_bigChange(zoneName string, ttl int) string { return fmt.Sprintf(` resource "google_dns_managed_zone" "parent-zone" { name = "%s" - dns_name = "terraform.test." + dns_name = "hashicorptest.com." description = "Test Description" } resource "google_dns_record_set" "foobar" { managed_zone = "${google_dns_managed_zone.parent-zone.name}" - name = "test-record.terraform.test." + name = "test-record.hashicorptest.com." type = "CNAME" rrdatas = ["www.terraform.io."] ttl = %d From 43d968cabeadcd87eab532685c73ed6746358c2f Mon Sep 17 00:00:00 2001 From: Dana Hoffman Date: Thu, 2 Feb 2017 13:36:22 -0800 Subject: [PATCH 16/24] provider/google: set subnetwork_project to computed --- resource_compute_instance_template.go | 1 + 1 file changed, 1 insertion(+) diff --git a/resource_compute_instance_template.go b/resource_compute_instance_template.go index fa0f5b6b..e34b2c2c 100644 --- a/resource_compute_instance_template.go +++ b/resource_compute_instance_template.go @@ -207,6 +207,7 @@ func resourceComputeInstanceTemplate() *schema.Resource { Type: schema.TypeString, Optional: true, ForceNew: true, + Computed: true, }, "access_config": &schema.Schema{ From c6b1b37eb5bb8307aa3f356ffaf087ff9cc11408 Mon Sep 17 00:00:00 2001 From: Dana Hoffman Date: Thu, 9 Feb 2017 16:14:00 -0800 Subject: [PATCH 17/24] provider/google: Update node version in container cluster test --- resource_container_cluster_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resource_container_cluster_test.go b/resource_container_cluster_test.go index 6359ab42..4f4ff820 100644 --- a/resource_container_cluster_test.go +++ b/resource_container_cluster_test.go @@ -215,7 +215,7 @@ var testAccContainerCluster_withVersion = fmt.Sprintf(` resource "google_container_cluster" "with_version" { name = "cluster-test-%s" zone = "us-central1-a" - node_version = "1.4.7" + node_version = "1.5.2" initial_node_count = 1 master_auth { From f353843c2f885d3029fae4c8841dc9a594bbcde0 Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Tue, 14 Feb 2017 23:35:17 +0000 Subject: [PATCH 18/24] provider/google: Add google_compute_zones data source --- data_source_google_compute_zones.go | 80 ++++++++++++++++++++++++ data_source_google_compute_zones_test.go | 70 +++++++++++++++++++++ provider.go | 3 +- 3 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 data_source_google_compute_zones.go create mode 100644 data_source_google_compute_zones_test.go diff --git a/data_source_google_compute_zones.go b/data_source_google_compute_zones.go new file mode 100644 index 00000000..a200aba5 --- /dev/null +++ b/data_source_google_compute_zones.go @@ -0,0 +1,80 @@ +package google + +import ( + "fmt" + "log" + "sort" + "time" + + "github.com/hashicorp/terraform/helper/schema" + compute "google.golang.org/api/compute/v1" +) + +func dataSourceGoogleComputeZones() *schema.Resource { + return &schema.Resource{ + Read: dataSourceGoogleComputeZonesRead, + Schema: map[string]*schema.Schema{ + "region": { + Type: schema.TypeString, + Optional: true, + }, + "names": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "status": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { + value := v.(string) + if value != "UP" && value != "DOWN" { + es = append(es, fmt.Errorf("%q can only be 'UP' or 'DOWN' (%q given)", k, value)) + } + return + }, + }, + }, + } +} + +func dataSourceGoogleComputeZonesRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + region := config.Region + if r, ok := d.GetOk("region"); ok { + region = r.(string) + } + + regionUrl := fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/regions/%s", + config.Project, region) + filter := fmt.Sprintf("(region eq %s)", regionUrl) + + if s, ok := d.GetOk("status"); ok { + filter += fmt.Sprintf(" (status eq %s)", s) + } + + call := config.clientCompute.Zones.List(config.Project).Filter(filter) + + resp, err := call.Do() + if err != nil { + return err + } + + zones := flattenZones(resp.Items) + log.Printf("[DEBUG] Received Google Compute Zones: %q", zones) + + d.Set("names", zones) + d.SetId(time.Now().UTC().String()) + + return nil +} + +func flattenZones(zones []*compute.Zone) []string { + result := make([]string, len(zones), len(zones)) + for i, zone := range zones { + result[i] = zone.Name + } + sort.Strings(result) + return result +} diff --git a/data_source_google_compute_zones_test.go b/data_source_google_compute_zones_test.go new file mode 100644 index 00000000..80dabf22 --- /dev/null +++ b/data_source_google_compute_zones_test.go @@ -0,0 +1,70 @@ +package google + +import ( + "errors" + "fmt" + "strconv" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccGoogleComputeZones_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccCheckGoogleComputeZonesConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleComputeZonesMeta("data.google_compute_zones.available"), + ), + }, + }, + }) +} + +func testAccCheckGoogleComputeZonesMeta(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Can't find zones data source: %s", n) + } + + if rs.Primary.ID == "" { + return errors.New("zones data source ID not set.") + } + + count, ok := rs.Primary.Attributes["names.#"] + if !ok { + return errors.New("can't find 'names' attribute") + } + + noOfNames, err := strconv.Atoi(count) + if err != nil { + return errors.New("failed to read number of zones") + } + if noOfNames < 2 { + return fmt.Errorf("expected at least 2 zones, received %d, this is most likely a bug", + noOfNames) + } + + for i := 0; i < noOfNames; i++ { + idx := "names." + strconv.Itoa(i) + v, ok := rs.Primary.Attributes[idx] + if !ok { + return fmt.Errorf("zone list is corrupt (%q not found), this is definitely a bug", idx) + } + if len(v) < 1 { + return fmt.Errorf("Empty zone name (%q), this is definitely a bug", idx) + } + } + + return nil + } +} + +var testAccCheckGoogleComputeZonesConfig = ` +data "google_compute_zones" "available" {} +` diff --git a/provider.go b/provider.go index d1263efa..f4d7d5f7 100644 --- a/provider.go +++ b/provider.go @@ -57,7 +57,8 @@ func Provider() terraform.ResourceProvider { }, DataSourcesMap: map[string]*schema.Resource{ - "google_iam_policy": dataSourceGoogleIamPolicy(), + "google_iam_policy": dataSourceGoogleIamPolicy(), + "google_compute_zones": dataSourceGoogleComputeZones(), }, ResourcesMap: map[string]*schema.Resource{ From 8b82adfdbe135f0e7414af694cd6781ab267e4f0 Mon Sep 17 00:00:00 2001 From: Roberto Jung Drebes Date: Thu, 2 Feb 2017 13:06:43 +0100 Subject: [PATCH 19/24] provider/google: make local_traffic_selector computed now that we read it back from server --- resource_compute_vpn_tunnel.go | 1 + resource_compute_vpn_tunnel_test.go | 63 +++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/resource_compute_vpn_tunnel.go b/resource_compute_vpn_tunnel.go index 7989035d..42f477d9 100644 --- a/resource_compute_vpn_tunnel.go +++ b/resource_compute_vpn_tunnel.go @@ -68,6 +68,7 @@ func resourceComputeVpnTunnel() *schema.Resource { Type: schema.TypeSet, Optional: true, ForceNew: true, + Computed: true, Elem: &schema.Schema{Type: schema.TypeString}, Set: schema.HashString, }, diff --git a/resource_compute_vpn_tunnel_test.go b/resource_compute_vpn_tunnel_test.go index c863fce6..dfd153e4 100644 --- a/resource_compute_vpn_tunnel_test.go +++ b/resource_compute_vpn_tunnel_test.go @@ -32,6 +32,22 @@ func TestAccComputeVpnTunnel_basic(t *testing.T) { }) } +func TestAccComputeVpnTunnel_defaultTrafficSelectors(t *testing.T) { + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeVpnTunnelDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeVpnTunnelDefaultTrafficSelectors, + Check: testAccCheckComputeVpnTunnelExists( + "google_compute_vpn_tunnel.foobar"), + }, + }, + }) +} + func testAccCheckComputeVpnTunnelDestroy(s *terraform.State) error { config := testAccProvider.Meta().(*Config) project := config.Project @@ -137,3 +153,50 @@ resource "google_compute_vpn_tunnel" "foobar" { }`, acctest.RandString(10), acctest.RandString(10), acctest.RandString(10), acctest.RandString(10), acctest.RandString(10), acctest.RandString(10), acctest.RandString(10), acctest.RandString(10)) + +var testAccComputeVpnTunnelDefaultTrafficSelectors = fmt.Sprintf(` +resource "google_compute_network" "foobar" { + name = "tunnel-test-%s" + auto_create_subnetworks = "true" +} +resource "google_compute_address" "foobar" { + name = "tunnel-test-%s" + region = "us-central1" +} +resource "google_compute_vpn_gateway" "foobar" { + name = "tunnel-test-%s" + network = "${google_compute_network.foobar.self_link}" + region = "${google_compute_address.foobar.region}" +} +resource "google_compute_forwarding_rule" "foobar_esp" { + name = "tunnel-test-%s" + region = "${google_compute_vpn_gateway.foobar.region}" + ip_protocol = "ESP" + ip_address = "${google_compute_address.foobar.address}" + target = "${google_compute_vpn_gateway.foobar.self_link}" +} +resource "google_compute_forwarding_rule" "foobar_udp500" { + name = "tunnel-test-%s" + region = "${google_compute_forwarding_rule.foobar_esp.region}" + ip_protocol = "UDP" + port_range = "500-500" + ip_address = "${google_compute_address.foobar.address}" + target = "${google_compute_vpn_gateway.foobar.self_link}" +} +resource "google_compute_forwarding_rule" "foobar_udp4500" { + name = "tunnel-test-%s" + region = "${google_compute_forwarding_rule.foobar_udp500.region}" + ip_protocol = "UDP" + port_range = "4500-4500" + ip_address = "${google_compute_address.foobar.address}" + target = "${google_compute_vpn_gateway.foobar.self_link}" +} +resource "google_compute_vpn_tunnel" "foobar" { + name = "tunnel-test-%s" + region = "${google_compute_forwarding_rule.foobar_udp4500.region}" + target_vpn_gateway = "${google_compute_vpn_gateway.foobar.self_link}" + shared_secret = "unguessable" + peer_ip = "8.8.8.8" +}`, acctest.RandString(10), acctest.RandString(10), acctest.RandString(10), + acctest.RandString(10), acctest.RandString(10), acctest.RandString(10), + acctest.RandString(10)) From ecbbaaee8910d7829def31a026b50cc795512535 Mon Sep 17 00:00:00 2001 From: Paddy Date: Thu, 16 Feb 2017 02:36:03 -0800 Subject: [PATCH 20/24] Check for errors when deleting project metadata. Our delete operation for google_compute_project_metadata didn't check an error when making the call to delete metadata, which led to a panic in our tests. This is also probably indicative of why our tests failed/metadata got left dangling. --- resource_compute_project_metadata.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/resource_compute_project_metadata.go b/resource_compute_project_metadata.go index ea8a5128..6b867e1a 100644 --- a/resource_compute_project_metadata.go +++ b/resource_compute_project_metadata.go @@ -192,6 +192,10 @@ func resourceComputeProjectMetadataDelete(d *schema.ResourceData, meta interface op, err := config.clientCompute.Projects.SetCommonInstanceMetadata(projectID, md).Do() + if err != nil { + return fmt.Errorf("Error removing metadata from project %s: %s", projectID, err) + } + log.Printf("[DEBUG] SetCommonMetadata: %d (%s)", op.Id, op.SelfLink) err = computeOperationWaitGlobal(config, op, project.Name, "SetCommonMetadata") From 93a3364b2e7d79e36887b6fa5644dfebde86fc63 Mon Sep 17 00:00:00 2001 From: Sebastien Badia Date: Fri, 17 Feb 2017 15:59:25 +0100 Subject: [PATCH 21/24] doc: gcs - Update storage_class documentation and tests `STANDARD` storage_class is now replaced by `MULTI_REGIONAL` depending the bucket location. Same for `DURABLE_REDUCED_AVAILABILITY` replaced by `REGIONAL`. refs: https://cloud.google.com/storage/docs/storage-classes#standard --- resource_storage_bucket_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/resource_storage_bucket_test.go b/resource_storage_bucket_test.go index 2e1a9e2b..59591639 100644 --- a/resource_storage_bucket_test.go +++ b/resource_storage_bucket_test.go @@ -68,12 +68,12 @@ func TestAccStorageStorageClass(t *testing.T) { CheckDestroy: testAccGoogleStorageDestroy, Steps: []resource.TestStep{ { - Config: testGoogleStorageBucketsReaderStorageClass(bucketName, "STANDARD"), + Config: testGoogleStorageBucketsReaderStorageClass(bucketName, "MULTI_REGIONAL"), Check: resource.ComposeTestCheckFunc( testAccCheckCloudStorageBucketExists( "google_storage_bucket.bucket", bucketName), resource.TestCheckResourceAttr( - "google_storage_bucket.bucket", "storage_class", "STANDARD"), + "google_storage_bucket.bucket", "storage_class", "MULTI_REGIONAL"), ), }, { @@ -86,12 +86,12 @@ func TestAccStorageStorageClass(t *testing.T) { ), }, { - Config: testGoogleStorageBucketsReaderStorageClass(bucketName, "DURABLE_REDUCED_AVAILABILITY"), + Config: testGoogleStorageBucketsReaderStorageClass(bucketName, "REGIONAL"), Check: resource.ComposeTestCheckFunc( testAccCheckCloudStorageBucketExists( "google_storage_bucket.bucket", bucketName), resource.TestCheckResourceAttr( - "google_storage_bucket.bucket", "storage_class", "DURABLE_REDUCED_AVAILABILITY"), + "google_storage_bucket.bucket", "storage_class", "REGIONAL"), ), }, }, From b1b6993d23b2352cdf473cf283a4915caafe748b Mon Sep 17 00:00:00 2001 From: Mike Fowler Date: Fri, 17 Feb 2017 23:33:47 +0000 Subject: [PATCH 22/24] provider/google-cloud: Add maintenance window (#12042) * provider/google-cloud: Add maintenance window Allows specification of the `maintenance_window` within the `settings` block. This controls when Google will restart a database in order to apply updates. It is also possible to select an `update_track` to relatively control updating between instances in the same project. * Adjustments as suggested in code review. --- resource_sql_database_instance.go | 93 ++++++++++++++++++++++++++ resource_sql_database_instance_test.go | 60 +++++++++++++++++ 2 files changed, 153 insertions(+) diff --git a/resource_sql_database_instance.go b/resource_sql_database_instance.go index ee03391e..2a1fa2f3 100644 --- a/resource_sql_database_instance.go +++ b/resource_sql_database_instance.go @@ -153,6 +153,33 @@ func resourceSqlDatabaseInstance() *schema.Resource { }, }, }, + "maintenance_window": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "day": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { + return validateNumericRange(v, k, 1, 7) + }, + }, + "hour": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { + return validateNumericRange(v, k, 0, 23) + }, + }, + "update_track": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, "pricing_plan": &schema.Schema{ Type: schema.TypeString, Optional: true, @@ -431,6 +458,25 @@ func resourceSqlDatabaseInstanceCreate(d *schema.ResourceData, meta interface{}) } } + if v, ok := _settings["maintenance_window"]; ok && len(v.([]interface{})) > 0 { + settings.MaintenanceWindow = &sqladmin.MaintenanceWindow{} + _maintenanceWindow := v.([]interface{})[0].(map[string]interface{}) + + if vp, okp := _maintenanceWindow["day"]; okp { + settings.MaintenanceWindow.Day = int64(vp.(int)) + } + + if vp, okp := _maintenanceWindow["hour"]; okp { + settings.MaintenanceWindow.Hour = int64(vp.(int)) + } + + if vp, ok := _maintenanceWindow["update_track"]; ok { + if len(vp.(string)) > 0 { + settings.MaintenanceWindow.UpdateTrack = vp.(string) + } + } + } + if v, ok := _settings["pricing_plan"]; ok { settings.PricingPlan = v.(string) } @@ -745,6 +791,25 @@ func resourceSqlDatabaseInstanceRead(d *schema.ResourceData, meta interface{}) e } } + if v, ok := _settings["maintenance_window"]; ok && len(v.([]interface{})) > 0 && + settings.MaintenanceWindow != nil { + _maintenanceWindow := v.([]interface{})[0].(map[string]interface{}) + + if vp, okp := _maintenanceWindow["day"]; okp && vp != nil { + _maintenanceWindow["day"] = settings.MaintenanceWindow.Day + } + + if vp, okp := _maintenanceWindow["hour"]; okp && vp != nil { + _maintenanceWindow["hour"] = settings.MaintenanceWindow.Hour + } + + if vp, ok := _maintenanceWindow["update_track"]; ok && vp != nil { + if len(vp.(string)) > 0 { + _maintenanceWindow["update_track"] = settings.MaintenanceWindow.UpdateTrack + } + } + } + if v, ok := _settings["pricing_plan"]; ok && len(v.(string)) > 0 { _settings["pricing_plan"] = settings.PricingPlan } @@ -1062,6 +1127,25 @@ func resourceSqlDatabaseInstanceUpdate(d *schema.ResourceData, meta interface{}) } } + if v, ok := _settings["maintenance_window"]; ok && len(v.([]interface{})) > 0 { + settings.MaintenanceWindow = &sqladmin.MaintenanceWindow{} + _maintenanceWindow := v.([]interface{})[0].(map[string]interface{}) + + if vp, okp := _maintenanceWindow["day"]; okp { + settings.MaintenanceWindow.Day = int64(vp.(int)) + } + + if vp, okp := _maintenanceWindow["hour"]; okp { + settings.MaintenanceWindow.Hour = int64(vp.(int)) + } + + if vp, ok := _maintenanceWindow["update_track"]; ok { + if len(vp.(string)) > 0 { + settings.MaintenanceWindow.UpdateTrack = vp.(string) + } + } + } + if v, ok := _settings["pricing_plan"]; ok { settings.PricingPlan = v.(string) } @@ -1109,3 +1193,12 @@ func resourceSqlDatabaseInstanceDelete(d *schema.ResourceData, meta interface{}) return nil } + +func validateNumericRange(v interface{}, k string, min int, max int) (ws []string, errors []error) { + value := v.(int) + if min > value || value > max { + errors = append(errors, fmt.Errorf( + "%q outside range %d-%d.", k, min, max)) + } + return +} diff --git a/resource_sql_database_instance_test.go b/resource_sql_database_instance_test.go index 0a0b1bcc..4734fac6 100644 --- a/resource_sql_database_instance_test.go +++ b/resource_sql_database_instance_test.go @@ -162,6 +162,29 @@ func TestAccGoogleSqlDatabaseInstance_diskspecs(t *testing.T) { }) } +func TestAccGoogleSqlDatabaseInstance_maintenance(t *testing.T) { + var instance sqladmin.DatabaseInstance + masterID := acctest.RandInt() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccGoogleSqlDatabaseInstanceDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: fmt.Sprintf( + testGoogleSqlDatabaseInstance_maintenance, masterID), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleSqlDatabaseInstanceExists( + "google_sql_database_instance.instance", &instance), + testAccCheckGoogleSqlDatabaseInstanceEquals( + "google_sql_database_instance.instance", &instance), + ), + }, + }, + }) +} + func TestAccGoogleSqlDatabaseInstance_settings_upgrade(t *testing.T) { var instance sqladmin.DatabaseInstance databaseID := acctest.RandInt() @@ -359,6 +382,26 @@ func testAccCheckGoogleSqlDatabaseInstanceEquals(n string, } } + if instance.Settings.MaintenanceWindow != nil { + server = strconv.FormatInt(instance.Settings.MaintenanceWindow.Day, 10) + local = attributes["settings.0.maintenance_window.0.day"] + if server != local && len(server) > 0 && len(local) > 0 { + return fmt.Errorf("Error settings.maintenance_window.day mismatch, (%s, %s)", server, local) + } + + server = strconv.FormatInt(instance.Settings.MaintenanceWindow.Hour, 10) + local = attributes["settings.0.maintenance_window.0.hour"] + if server != local && len(server) > 0 && len(local) > 0 { + return fmt.Errorf("Error settings.maintenance_window.hour mismatch, (%s, %s)", server, local) + } + + server = instance.Settings.MaintenanceWindow.UpdateTrack + local = attributes["settings.0.maintenance_window.0.update_track"] + if server != local && len(server) > 0 && len(local) > 0 { + return fmt.Errorf("Error settings.maintenance_window.update_track mismatch, (%s, %s)", server, local) + } + } + server = instance.Settings.PricingPlan local = attributes["settings.0.pricing_plan"] if server != local && len(server) > 0 && len(local) > 0 { @@ -639,6 +682,23 @@ resource "google_sql_database_instance" "instance" { } ` +var testGoogleSqlDatabaseInstance_maintenance = ` +resource "google_sql_database_instance" "instance" { + name = "tf-lw-%d" + region = "us-central1" + + settings { + tier = "db-f1-micro" + + maintenance_window { + day = 7 + hour = 3 + update_track = "canary" + } + } +} +` + var testGoogleSqlDatabaseInstance_authNets_step1 = ` resource "google_sql_database_instance" "instance" { name = "tf-lw-%d" From 108ef0e4f71c215ebfab8a7b5e26cd3373320602 Mon Sep 17 00:00:00 2001 From: Dana Hoffman Date: Mon, 20 Feb 2017 09:28:32 -0800 Subject: [PATCH 23/24] provider/google: Write the raw disk encryption key in the state file to avoid diffs on plan (#12068) --- resource_compute_instance.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/resource_compute_instance.go b/resource_compute_instance.go index 1c7bd021..46daaf31 100644 --- a/resource_compute_instance.go +++ b/resource_compute_instance.go @@ -794,13 +794,14 @@ func resourceComputeInstanceRead(d *schema.ResourceData, meta interface{}) error disks := make([]map[string]interface{}, 0, 1) for i, disk := range instance.Disks { di := map[string]interface{}{ - "disk": d.Get(fmt.Sprintf("disk.%d.disk", i)), - "image": d.Get(fmt.Sprintf("disk.%d.image", i)), - "type": d.Get(fmt.Sprintf("disk.%d.type", i)), - "scratch": d.Get(fmt.Sprintf("disk.%d.scratch", i)), - "auto_delete": d.Get(fmt.Sprintf("disk.%d.auto_delete", i)), - "size": d.Get(fmt.Sprintf("disk.%d.size", i)), - "device_name": d.Get(fmt.Sprintf("disk.%d.device_name", i)), + "disk": d.Get(fmt.Sprintf("disk.%d.disk", i)), + "image": d.Get(fmt.Sprintf("disk.%d.image", i)), + "type": d.Get(fmt.Sprintf("disk.%d.type", i)), + "scratch": d.Get(fmt.Sprintf("disk.%d.scratch", i)), + "auto_delete": d.Get(fmt.Sprintf("disk.%d.auto_delete", i)), + "size": d.Get(fmt.Sprintf("disk.%d.size", i)), + "device_name": d.Get(fmt.Sprintf("disk.%d.device_name", i)), + "disk_encryption_key_raw": d.Get(fmt.Sprintf("disk.%d.disk_encryption_key_raw", i)), } if disk.DiskEncryptionKey != nil && disk.DiskEncryptionKey.Sha256 != "" { di["disk_encryption_key_sha256"] = disk.DiskEncryptionKey.Sha256 From bf10e5519df1d6a9d7cf70170c2ec57b587b2380 Mon Sep 17 00:00:00 2001 From: Evan Brown Date: Mon, 20 Feb 2017 09:32:24 -0800 Subject: [PATCH 24/24] providers/google: google_project supports billing account (#11653) * Vendor google.golang.org/api/cloudbilling/v1 * providers/google: Add cloudbilling client * providers/google: google_project supports billing account This change allows a Terraform user to set and update the billing account associated with their project. * providers/google: Testing project billing account This change adds optional acceptance tests for project billing accounts. GOOGLE_PROJECT_BILLING_ACCOUNT and GOOGLE_PROJECT_BILLING_ACCOUNT_2 must be set in the environment for the tests to run; otherwise, they will be skipped. Also includes a few code cleanups per review. * providers/google: Improve project billing error message --- config.go | 9 ++ resource_google_project.go | 58 ++++++++++++ resource_google_project_iam_policy_test.go | 10 ++ resource_google_project_test.go | 105 +++++++++++++++++++++ 4 files changed, 182 insertions(+) diff --git a/config.go b/config.go index 9f9eb075..37ac2db8 100644 --- a/config.go +++ b/config.go @@ -13,6 +13,7 @@ import ( "golang.org/x/oauth2" "golang.org/x/oauth2/google" "golang.org/x/oauth2/jwt" + "google.golang.org/api/cloudbilling/v1" "google.golang.org/api/cloudresourcemanager/v1" "google.golang.org/api/compute/v1" "google.golang.org/api/container/v1" @@ -31,6 +32,7 @@ type Config struct { Project string Region string + clientBilling *cloudbilling.Service clientCompute *compute.Service clientContainer *container.Service clientDns *dns.Service @@ -160,6 +162,13 @@ func (c *Config) loadAndValidate() error { } c.clientServiceMan.UserAgent = userAgent + log.Printf("[INFO] Instantiating Google Cloud Billing Client...") + c.clientBilling, err = cloudbilling.New(client) + if err != nil { + return err + } + c.clientBilling.UserAgent = userAgent + return nil } diff --git a/resource_google_project.go b/resource_google_project.go index 24dc56b8..b4bcb9c4 100644 --- a/resource_google_project.go +++ b/resource_google_project.go @@ -6,8 +6,10 @@ import ( "log" "net/http" "strconv" + "strings" "github.com/hashicorp/terraform/helper/schema" + "google.golang.org/api/cloudbilling/v1" "google.golang.org/api/cloudresourcemanager/v1" "google.golang.org/api/googleapi" ) @@ -86,6 +88,10 @@ func resourceGoogleProject() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "billing_account": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, }, } } @@ -172,6 +178,22 @@ func resourceGoogleProjectCreate(d *schema.ResourceData, meta interface{}) error } } + // Set the billing account + if v, ok := d.GetOk("billing_account"); ok { + name := v.(string) + ba := cloudbilling.ProjectBillingInfo{ + BillingAccountName: "billingAccounts/" + name, + } + _, err = config.clientBilling.Projects.UpdateBillingInfo(prefixedProject(pid), &ba).Do() + if err != nil { + d.Set("billing_account", "") + if _err, ok := err.(*googleapi.Error); ok { + return fmt.Errorf("Error setting billing account %q for project %q: %v", name, prefixedProject(pid), _err) + } + return fmt.Errorf("Error setting billing account %q for project %q: %v", name, prefixedProject(pid), err) + } + } + return resourceGoogleProjectRead(d, meta) } @@ -196,9 +218,30 @@ func resourceGoogleProjectRead(d *schema.ResourceData, meta interface{}) error { d.Set("org_id", p.Parent.Id) } + // Read the billing account + ba, err := config.clientBilling.Projects.GetBillingInfo(prefixedProject(pid)).Do() + if err != nil { + return fmt.Errorf("Error reading billing account for project %q: %v", prefixedProject(pid), err) + } + if ba.BillingAccountName != "" { + // BillingAccountName is contains the resource name of the billing account + // associated with the project, if any. For example, + // `billingAccounts/012345-567890-ABCDEF`. We care about the ID and not + // the `billingAccounts/` prefix, so we need to remove that. If the + // prefix ever changes, we'll validate to make sure it's something we + // recognize. + _ba := strings.TrimPrefix(ba.BillingAccountName, "billingAccounts/") + if ba.BillingAccountName == _ba { + return fmt.Errorf("Error parsing billing account for project %q. Expected value to begin with 'billingAccounts/' but got %s", prefixedProject(pid), ba.BillingAccountName) + } + d.Set("billing_account", _ba) + } return nil } +func prefixedProject(pid string) string { + return "projects/" + pid +} func resourceGoogleProjectUpdate(d *schema.ResourceData, meta interface{}) error { config := meta.(*Config) pid := d.Id() @@ -224,6 +267,21 @@ func resourceGoogleProjectUpdate(d *schema.ResourceData, meta interface{}) error } } + // Billing account has changed + if ok := d.HasChange("billing_account"); ok { + name := d.Get("billing_account").(string) + ba := cloudbilling.ProjectBillingInfo{ + BillingAccountName: "billingAccounts/" + name, + } + _, err = config.clientBilling.Projects.UpdateBillingInfo(prefixedProject(pid), &ba).Do() + if err != nil { + d.Set("billing_account", "") + if _err, ok := err.(*googleapi.Error); ok { + return fmt.Errorf("Error updating billing account %q for project %q: %v", name, prefixedProject(pid), _err) + } + return fmt.Errorf("Error updating billing account %q for project %q: %v", name, prefixedProject(pid), err) + } + } return updateProjectIamPolicy(d, config, pid) } diff --git a/resource_google_project_iam_policy_test.go b/resource_google_project_iam_policy_test.go index 57e9a296..59903ca8 100644 --- a/resource_google_project_iam_policy_test.go +++ b/resource_google_project_iam_policy_test.go @@ -624,3 +624,13 @@ resource "google_project" "acceptance" { org_id = "%s" }`, pid, name, org) } + +func testAccGoogleProject_createBilling(pid, name, org, billing string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + name = "%s" + org_id = "%s" + billing_account = "%s" +}`, pid, name, org, billing) +} diff --git a/resource_google_project_test.go b/resource_google_project_test.go index 03bdeee0..8381cb33 100644 --- a/resource_google_project_test.go +++ b/resource_google_project_test.go @@ -3,6 +3,7 @@ package google import ( "fmt" "os" + "strings" "testing" "github.com/hashicorp/terraform/helper/acctest" @@ -48,6 +49,76 @@ func TestAccGoogleProject_create(t *testing.T) { }) } +// Test that a Project resource can be created with an associated +// billing account +func TestAccGoogleProject_createBilling(t *testing.T) { + skipIfEnvNotSet(t, + []string{ + "GOOGLE_ORG", + "GOOGLE_BILLING_ACCOUNT", + }..., + ) + + billingId := os.Getenv("GOOGLE_BILLING_ACCOUNT") + pid := "terraform-" + acctest.RandString(10) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + // This step creates a new project with a billing account + resource.TestStep{ + Config: testAccGoogleProject_createBilling(pid, pname, org, billingId), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleProjectHasBillingAccount("google_project.acceptance", pid, billingId), + ), + }, + }, + }) +} + +// Test that a Project resource can be created and updated +// with billing account information +func TestAccGoogleProject_updateBilling(t *testing.T) { + skipIfEnvNotSet(t, + []string{ + "GOOGLE_ORG", + "GOOGLE_BILLING_ACCOUNT", + "GOOGLE_BILLING_ACCOUNT_2", + }..., + ) + + billingId := os.Getenv("GOOGLE_BILLING_ACCOUNT") + billingId2 := os.Getenv("GOOGLE_BILLING_ACCOUNT_2") + pid := "terraform-" + acctest.RandString(10) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + // This step creates a new project without a billing account + resource.TestStep{ + Config: testAccGoogleProject_create(pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleProjectExists("google_project.acceptance", pid), + ), + }, + // Update to include a billing account + resource.TestStep{ + Config: testAccGoogleProject_createBilling(pid, pname, org, billingId), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleProjectHasBillingAccount("google_project.acceptance", pid, billingId), + ), + }, + // Update to a different billing account + resource.TestStep{ + Config: testAccGoogleProject_createBilling(pid, pname, org, billingId2), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleProjectHasBillingAccount("google_project.acceptance", pid, billingId2), + ), + }, + }, + }) +} + // Test that a Project resource merges the IAM policies that already // exist, and won't lock people out. func TestAccGoogleProject_merge(t *testing.T) { @@ -95,6 +166,32 @@ func testAccCheckGoogleProjectExists(r, pid string) resource.TestCheckFunc { } } +func testAccCheckGoogleProjectHasBillingAccount(r, pid, billingId string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[r] + if !ok { + return fmt.Errorf("Not found: %s", r) + } + + // State should match expected + if rs.Primary.Attributes["billing_account"] != billingId { + return fmt.Errorf("Billing ID in state (%s) does not match expected value (%s)", rs.Primary.Attributes["billing_account"], billingId) + } + + // Actual value in API should match state and expected + // Read the billing account + config := testAccProvider.Meta().(*Config) + ba, err := config.clientBilling.Projects.GetBillingInfo(prefixedProject(pid)).Do() + if err != nil { + return fmt.Errorf("Error reading billing account for project %q: %v", prefixedProject(pid), err) + } + if billingId != strings.TrimPrefix(ba.BillingAccountName, "billingAccounts/") { + return fmt.Errorf("Billing ID returned by API (%s) did not match expected value (%s)", ba.BillingAccountName, billingId) + } + return nil + } +} + func testAccCheckGoogleProjectHasMoreBindingsThan(pid string, count int) resource.TestCheckFunc { return func(s *terraform.State) error { policy, err := getProjectIamPolicy(pid, testAccProvider.Meta().(*Config)) @@ -167,3 +264,11 @@ resource "google_project" "acceptance" { org_id = "%s" }`, pid, name, org) } + +func skipIfEnvNotSet(t *testing.T, envs ...string) { + for _, k := range envs { + if os.Getenv(k) == "" { + t.Skipf("Environment variable %s is not set", k) + } + } +}