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/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{ diff --git a/resource_compute_instance.go b/resource_compute_instance.go index c25cd87c..46daaf31 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 { @@ -790,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 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, "/") diff --git a/resource_compute_instance_template.go b/resource_compute_instance_template.go index 9b9798dc..e34b2c2c 100644 --- a/resource_compute_instance_template.go +++ b/resource_compute_instance_template.go @@ -207,11 +207,13 @@ func resourceComputeInstanceTemplate() *schema.Resource { Type: schema.TypeString, Optional: true, ForceNew: true, + Computed: true, }, "access_config": &schema.Schema{ Type: schema.TypeList, Optional: true, + ForceNew: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "nat_ip": &schema.Schema{ 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) 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") diff --git a/resource_compute_vpn_tunnel.go b/resource_compute_vpn_tunnel.go index 989764c2..42f477d9 100644 --- a/resource_compute_vpn_tunnel.go +++ b/resource_compute_vpn_tunnel.go @@ -65,6 +65,15 @@ func resourceComputeVpnTunnel() *schema.Resource { }, "local_traffic_selector": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + + "remote_traffic_selector": &schema.Schema{ Type: schema.TypeSet, Optional: true, ForceNew: true, @@ -124,15 +133,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 { @@ -182,6 +200,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..dfd153e4 100644 --- a/resource_compute_vpn_tunnel_test.go +++ b/resource_compute_vpn_tunnel_test.go @@ -22,12 +22,32 @@ 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"), ), }, }, }) } +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 @@ -83,7 +103,61 @@ 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 = "${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_subnetwork.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" + 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)) + +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" diff --git a/resource_container_cluster.go b/resource_container_cluster.go index 19ab48a9..fd9aa43a 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 } @@ -444,7 +441,17 @@ func resourceContainerClusterRead(d *schema.ResourceData, meta interface{}) erro d.Set("name", cluster.Name) d.Set("zone", cluster.Zone) - d.Set("additional_zones", cluster.Locations) + + locations := []string{} + if len(cluster.Locations) > 1 { + 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..4f4ff820 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 @@ -219,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 { 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 diff --git a/resource_google_project.go b/resource_google_project.go index 4bc26c45..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,23 +218,30 @@ func resourceGoogleProjectRead(d *schema.ResourceData, meta interface{}) error { d.Set("org_id", p.Parent.Id) } - // Read the IAM policy - pol, err := getProjectIamPolicy(pid, config) + // Read the billing account + ba, err := config.clientBilling.Projects.GetBillingInfo(prefixedProject(pid)).Do() if err != nil { - return err + return fmt.Errorf("Error reading billing account for project %q: %v", prefixedProject(pid), err) } - - polBytes, err := json.Marshal(pol) - if err != nil { - return 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) } - - d.Set("policy_etag", pol.Etag) - d.Set("policy_data", string(polBytes)) - return nil } +func prefixedProject(pid string) string { + return "projects/" + pid +} func resourceGoogleProjectUpdate(d *schema.ResourceData, meta interface{}) error { config := meta.(*Config) pid := d.Id() @@ -238,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.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 } 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 aa3c03c5..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,104 @@ 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) { + 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 +166,45 @@ 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)) + 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 +236,39 @@ 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) +} + +func skipIfEnvNotSet(t *testing.T, envs ...string) { + for _, k := range envs { + if os.Getenv(k) == "" { + t.Skipf("Environment variable %s is not set", k) + } + } +} diff --git a/resource_sql_database_instance.go b/resource_sql_database_instance.go index 128e4b74..2a1fa2f3 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, @@ -87,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, @@ -139,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, @@ -323,6 +364,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{}) @@ -405,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) } @@ -500,7 +572,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 { @@ -564,7 +659,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 } @@ -577,6 +672,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 @@ -678,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 } @@ -758,7 +890,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) @@ -840,6 +972,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{}) @@ -981,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) } @@ -1028,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 15207a18..4734fac6 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" @@ -63,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() @@ -86,6 +111,80 @@ 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_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_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() @@ -199,7 +298,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) @@ -237,6 +336,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"] @@ -265,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 { @@ -377,6 +514,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" @@ -397,6 +555,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" { @@ -474,6 +641,64 @@ 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_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_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" 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"), ), }, },