diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d53a24fa0a..c3d9be2354e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,27 +1,35 @@ -## 1.13.1 (Unreleased) +## 1.14.1 (Unreleased) +## 1.14.0 (June 07, 2018) FEATURES: -* **New Datasource**: `google_service_account` [GH-1535] -* **New Datasource**: `google_service_account_key` [GH-1535] +* **New Datasource**: `google_service_account` ([#1535](https://github.com/terraform-providers/terraform-provider-google/issues/1535)) +* **New Datasource**: `google_service_account_key` ([#1535](https://github.com/terraform-providers/terraform-provider-google/issues/1535)) +* **New Datasource**: `google_netblock_ip_ranges` ([#1580](https://github.com/terraform-providers/terraform-provider-google/issues/1580)) +* **New Datasource**: `google_compute_regions` ([#1603](https://github.com/terraform-providers/terraform-provider-google/issues/1603)) IMPROVEMENTS: -* compute: As part of migrating `google_compute_disk` to be autogenerated, enabled encrypted source snapshot & images. [GH-1521]. -* compute: Accept subnetwork name only in `google_forwarding_rule` [GH-1552] -* compute: Add disabled property to `google_compute_firewall` [GH-1536] -* compute: Add support for custom request headers in `google_compute_backend_service` [GH-1537] -* compute: Add support for `ssl_policy` to `google_compute_target_ssl_proxy` [GH-1568] -* cloudbuild: Use the project defined in `trigger_template` when creating a `google_cloudbuild_trigger` [GH-1556] -* kms: Add basic update for `google_kms_crypto_key` resource [GH-1511] -* project: Use default provider project for `google_project_services` if project field is empty [GH-1553] -* project: Added support for restoring default organization policies [GH-1477] -* project: Handle spurious Cloud API errors and performance issues for `google_project_service(s)` [GH-1565] -* sql: Add labels support in `sql_database_instance` [GH-1567] +* compute: As part of migrating `google_compute_disk` to be autogenerated, enabled encrypted source snapshot & images. [[#1521](https://github.com/terraform-providers/terraform-provider-google/issues/1521)]. +* compute: Accept subnetwork name only in `google_forwarding_rule` ([#1552](https://github.com/terraform-providers/terraform-provider-google/issues/1552)) +* compute: Add disabled property to `google_compute_firewall` ([#1536](https://github.com/terraform-providers/terraform-provider-google/issues/1536)) +* compute: Add support for custom request headers in `google_compute_backend_service` ([#1537](https://github.com/terraform-providers/terraform-provider-google/issues/1537)) +* compute: Add support for `ssl_policy` to `google_compute_target_ssl_proxy` ([#1568](https://github.com/terraform-providers/terraform-provider-google/issues/1568)) +* compute: Add support for `version`s in instance group manager ([#1499](https://github.com/terraform-providers/terraform-provider-google/issues/1499)) +* compute: Add support for `network_tier` to address, instance and instance_template ([#1530](https://github.com/terraform-providers/terraform-provider-google/issues/1530)) +* cloudbuild: Use the project defined in `trigger_template` when creating a `google_cloudbuild_trigger` ([#1556](https://github.com/terraform-providers/terraform-provider-google/issues/1556)) +* cloudbuild: Support configuration file in repository for `google_cloudbuild_trigger` ([#1557](https://github.com/terraform-providers/terraform-provider-google/issues/1557)) +* kms: Add basic update for `google_kms_crypto_key` resource ([#1511](https://github.com/terraform-providers/terraform-provider-google/issues/1511)) +* project: Use default provider project for `google_project_services` if project field is empty ([#1553](https://github.com/terraform-providers/terraform-provider-google/issues/1553)) +* project: Added support for restoring default organization policies ([#1477](https://github.com/terraform-providers/terraform-provider-google/issues/1477)) +* project: Handle spurious Cloud API errors and performance issues for `google_project_service(s)` ([#1565](https://github.com/terraform-providers/terraform-provider-google/issues/1565)) +* redis: Add update support for Redis Instances ([#1590](https://github.com/terraform-providers/terraform-provider-google/issues/1590)) +* sql: Add labels support in `sql_database_instance` ([#1567](https://github.com/terraform-providers/terraform-provider-google/issues/1567)) BUG FIXES: -* dns: Suppress diff for ipv6 address in `google_dns_record_set` [GH-1551] -* storage: Support removing a label in `google_storage_bucket` [GH-1550] -* compute: Fix perpetual diff caused by the `google_instance_group` self_link in `google_regional_instance_group_manager` [GH-1549] -* project: Retry while listing enabled services [GH-1573] +* dns: Suppress diff for ipv6 address in `google_dns_record_set` ([#1551](https://github.com/terraform-providers/terraform-provider-google/issues/1551)) +* storage: Support removing a label in `google_storage_bucket` ([#1550](https://github.com/terraform-providers/terraform-provider-google/issues/1550)) +* compute: Fix perpetual diff caused by the `google_instance_group` self_link in `google_regional_instance_group_manager` ([#1549](https://github.com/terraform-providers/terraform-provider-google/issues/1549)) +* project: Retry while listing enabled services ([#1573](https://github.com/terraform-providers/terraform-provider-google/issues/1573)) +* redis: Allow self links for redis authorized network ([#1599](https://github.com/terraform-providers/terraform-provider-google/issues/1599)) ## 1.13.0 (May 24, 2018) diff --git a/google/compute_instance_helpers.go b/google/compute_instance_helpers.go index cc255df1c4b..ef6acf3f8f2 100644 --- a/google/compute_instance_helpers.go +++ b/google/compute_instance_helpers.go @@ -49,6 +49,7 @@ func flattenAccessConfigs(accessConfigs []*computeBeta.AccessConfig) ([]map[stri for i, ac := range accessConfigs { flattened[i] = map[string]interface{}{ "nat_ip": ac.NatIP, + "network_tier": ac.NetworkTier, "assigned_nat_ip": ac.NatIP, } if ac.SetPublicPtr { @@ -103,8 +104,9 @@ func expandAccessConfigs(configs []interface{}) []*computeBeta.AccessConfig { for i, raw := range configs { data := raw.(map[string]interface{}) acs[i] = &computeBeta.AccessConfig{ - Type: "ONE_TO_ONE_NAT", - NatIP: data["nat_ip"].(string), + Type: "ONE_TO_ONE_NAT", + NatIP: data["nat_ip"].(string), + NetworkTier: data["network_tier"].(string), } if ptr, ok := data["public_ptr_domain_name"]; ok && ptr != "" { acs[i].SetPublicPtr = true diff --git a/google/data_source_google_compute_regions.go b/google/data_source_google_compute_regions.go new file mode 100644 index 00000000000..683bbc4c774 --- /dev/null +++ b/google/data_source_google_compute_regions.go @@ -0,0 +1,73 @@ +package google + +import ( + "fmt" + "log" + "sort" + "time" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" + "google.golang.org/api/compute/v1" +) + +func dataSourceGoogleComputeRegions() *schema.Resource { + return &schema.Resource{ + Read: dataSourceGoogleComputeRegionsRead, + Schema: map[string]*schema.Schema{ + "project": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "names": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "status": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{"UP", "DOWN"}, false), + }, + }, + } +} + +func dataSourceGoogleComputeRegionsRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + project, err := getProject(d, config) + if err != nil { + return err + } + filter := "" + if s, ok := d.GetOk("status"); ok { + filter = fmt.Sprintf(" (status eq %s)", s) + } + + call := config.clientCompute.Regions.List(project).Filter(filter) + + resp, err := call.Do() + if err != nil { + return err + } + + regions := flattenRegions(resp.Items) + log.Printf("[DEBUG] Received Google Compute Regions: %q", regions) + + d.Set("names", regions) + d.Set("project", project) + d.SetId(time.Now().UTC().String()) + + return nil +} + +func flattenRegions(regions []*compute.Region) []string { + result := make([]string, len(regions), len(regions)) + for i, region := range regions { + result[i] = region.Name + } + sort.Strings(result) + return result +} diff --git a/google/data_source_google_compute_regions_test.go b/google/data_source_google_compute_regions_test.go new file mode 100644 index 00000000000..99a2c923f28 --- /dev/null +++ b/google/data_source_google_compute_regions_test.go @@ -0,0 +1,72 @@ +package google + +import ( + "errors" + "fmt" + "strconv" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccComputeRegions_basic(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccCheckGoogleComputeRegionsConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleComputeRegionsMeta("data.google_compute_regions.available"), + ), + }, + }, + }) +} + +func testAccCheckGoogleComputeRegionsMeta(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Can't find regions data source: %s", n) + } + + if rs.Primary.ID == "" { + return errors.New("regions 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 regions") + } + if noOfNames < 2 { + return fmt.Errorf("expected at least 2 regions, 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("region list is corrupt (%q not found), this is definitely a bug", idx) + } + if len(v) < 1 { + return fmt.Errorf("Empty region name (%q), this is definitely a bug", idx) + } + } + + return nil + } +} + +var testAccCheckGoogleComputeRegionsConfig = ` +data "google_compute_regions" "available" {} +` diff --git a/google/data_source_google_netblock_ip_ranges.go b/google/data_source_google_netblock_ip_ranges.go new file mode 100644 index 00000000000..15a286e1afc --- /dev/null +++ b/google/data_source_google_netblock_ip_ranges.go @@ -0,0 +1,128 @@ +package google + +import ( + "fmt" + "github.com/hashicorp/terraform/helper/schema" + "io/ioutil" + "net/http" + "strings" +) + +func dataSourceGoogleNetblockIpRanges() *schema.Resource { + return &schema.Resource{ + Read: dataSourceGoogleNetblockIpRangesRead, + + Schema: map[string]*schema.Schema{ + "cidr_blocks": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Computed: true, + }, + "cidr_blocks_ipv4": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Computed: true, + }, + "cidr_blocks_ipv6": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Computed: true, + }, + }, + } +} + +func dataSourceGoogleNetblockIpRangesRead(d *schema.ResourceData, meta interface{}) error { + d.SetId("netblock-ip-ranges") + + // https://cloud.google.com/compute/docs/faq#where_can_i_find_product_name_short_ip_ranges + CidrBlocks, err := getCidrBlocks() + + if err != nil { + return err + } + + d.Set("cidr_blocks", CidrBlocks["cidr_blocks"]) + d.Set("cidr_blocks_ipv4", CidrBlocks["cidr_blocks_ipv4"]) + d.Set("cidr_blocks_ipv6", CidrBlocks["cidr_blocks_ipv6"]) + + return nil +} + +func netblock_request(name string) (string, error) { + const DNS_URL = "https://dns.google.com/resolve?name=%s&type=TXT" + + response, err := http.Get(fmt.Sprintf("https://dns.google.com/resolve?name=%s&type=TXT", name)) + + if err != nil { + return "", fmt.Errorf("Error from _cloud-netblocks: %s", err) + } + + defer response.Body.Close() + body, err := ioutil.ReadAll(response.Body) + + if err != nil { + return "", fmt.Errorf("Error to retrieve the domains list: %s", err) + } + + return string(body), nil +} + +func getCidrBlocks() (map[string][]string, error) { + const INITIAL_NETBLOCK_DNS = "_cloud-netblocks.googleusercontent.com" + var dnsNetblockList []string + cidrBlocks := make(map[string][]string) + + response, err := netblock_request(INITIAL_NETBLOCK_DNS) + + if err != nil { + return nil, err + } + + splitedResponse := strings.Split(string(response), " ") + + for _, sp := range splitedResponse { + if strings.HasPrefix(sp, "include:") { + dnsNetblock := strings.Replace(sp, "include:", "", 1) + dnsNetblockList = append(dnsNetblockList, dnsNetblock) + } + } + + for len(dnsNetblockList) > 0 { + + dnsNetblock := dnsNetblockList[0] + + dnsNetblockList[0] = "" + dnsNetblockList = dnsNetblockList[1:len(dnsNetblockList)] + + response, err = netblock_request(dnsNetblock) + + if err != nil { + return nil, err + } + + splitedResponse = strings.Split(string(response), " ") + + for _, sp := range splitedResponse { + if strings.HasPrefix(sp, "ip") { + + cdrBlock := strings.Split(sp, ":")[1] + cidrBlocks["cidr_blocks"] = append(cidrBlocks["cidr_blocks"], cdrBlock) + + if strings.HasPrefix(sp, "ip4") { + cdrBlock := strings.Replace(sp, "ip4:", "", 1) + cidrBlocks["cidr_blocks_ipv4"] = append(cidrBlocks["cidr_blocks_ipv4"], cdrBlock) + + } else if strings.HasPrefix(sp, "ip6") { + cdrBlock := strings.Replace(sp, "ip6:", "", 1) + cidrBlocks["cidr_blocks_ipv6"] = append(cidrBlocks["cidr_blocks_ipv6"], cdrBlock) + } + } else if strings.HasPrefix(sp, "include:") { + cidr_block := strings.Replace(sp, "include:", "", 1) + dnsNetblockList = append(dnsNetblockList, cidr_block) + } + } + } + + return cidrBlocks, nil +} diff --git a/google/data_source_google_netblock_ip_ranges_test.go b/google/data_source_google_netblock_ip_ranges_test.go new file mode 100644 index 00000000000..b18f2c395c5 --- /dev/null +++ b/google/data_source_google_netblock_ip_ranges_test.go @@ -0,0 +1,38 @@ +package google + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform/helper/resource" +) + +func TestAccDataSourceGoogleNetblockIpRanges_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccNetblockIpRangesConfig, + Check: resource.ComposeTestCheckFunc( + resource.TestMatchResourceAttr("data.google_netblock_ip_ranges.some", + "cidr_blocks.#", regexp.MustCompile(("^[1-9]+[0-9]*$"))), + resource.TestMatchResourceAttr("data.google_netblock_ip_ranges.some", + "cidr_blocks.0", regexp.MustCompile("^[0-9./:]+$")), + resource.TestMatchResourceAttr("data.google_netblock_ip_ranges.some", + "cidr_blocks_ipv4.#", regexp.MustCompile(("^[1-9]+[0-9]*$"))), + resource.TestMatchResourceAttr("data.google_netblock_ip_ranges.some", + "cidr_blocks_ipv4.0", regexp.MustCompile("^[0-9./]+$")), + resource.TestMatchResourceAttr("data.google_netblock_ip_ranges.some", + "cidr_blocks_ipv6.#", regexp.MustCompile(("^[1-9]+[0-9]*$"))), + resource.TestMatchResourceAttr("data.google_netblock_ip_ranges.some", + "cidr_blocks_ipv6.0", regexp.MustCompile("^[0-9./:]+$")), + ), + }, + }, + }) +} + +const testAccNetblockIpRangesConfig = ` +data "google_netblock_ip_ranges" "some" {} +` diff --git a/google/provider.go b/google/provider.go index 6cde7bce9a8..01b53e53ce3 100644 --- a/google/provider.go +++ b/google/provider.go @@ -87,12 +87,14 @@ func Provider() terraform.ResourceProvider { "google_iam_policy": dataSourceGoogleIamPolicy(), "google_kms_secret": dataSourceGoogleKmsSecret(), "google_folder": dataSourceGoogleFolder(), + "google_netblock_ip_ranges": dataSourceGoogleNetblockIpRanges(), "google_organization": dataSourceGoogleOrganization(), "google_service_account": dataSourceGoogleServiceAccount(), "google_service_account_key": dataSourceGoogleServiceAccountKey(), "google_storage_object_signed_url": dataSourceGoogleSignedUrl(), "google_storage_project_service_account": dataSourceGoogleStorageProjectServiceAccount(), "google_compute_backend_service": dataSourceGoogleComputeBackendService(), + "google_compute_regions": dataSourceGoogleComputeRegions(), }, ResourcesMap: mergeResourceMaps( diff --git a/google/resource_cloudbuild_build_trigger.go b/google/resource_cloudbuild_build_trigger.go index 4a96e5a4315..278e8039cc0 100644 --- a/google/resource_cloudbuild_build_trigger.go +++ b/google/resource_cloudbuild_build_trigger.go @@ -36,6 +36,12 @@ func resourceCloudBuildTrigger() *schema.Resource { Computed: true, ForceNew: true, }, + "filename": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ConflictsWith: []string{"build"}, + }, "build": { Type: schema.TypeList, Description: "Contents of the build template.", @@ -142,7 +148,12 @@ func resourceCloudbuildBuildTriggerCreate(d *schema.ResourceData, meta interface buildTrigger.Description = v.(string) } - buildTrigger.Build = expandCloudbuildBuildTriggerBuild(d) + if v, ok := d.GetOk("filename"); ok { + buildTrigger.Filename = v.(string) + } else { + buildTrigger.Build = expandCloudbuildBuildTriggerBuild(d) + } + buildTrigger.TriggerTemplate = expandCloudbuildBuildTriggerTemplate(d, project) tstr, err := json.Marshal(buildTrigger) @@ -179,7 +190,10 @@ func resourceCloudbuildBuildTriggerRead(d *schema.ResourceData, meta interface{} if buildTrigger.TriggerTemplate != nil { d.Set("trigger_template", flattenCloudbuildBuildTriggerTemplate(d, config, buildTrigger.TriggerTemplate)) } - if buildTrigger.Build != nil { + + if buildTrigger.Filename != "" { + d.Set("filename", buildTrigger.Filename) + } else if buildTrigger.Build != nil { d.Set("build", flattenCloudbuildBuildTriggerBuild(d, config, buildTrigger.Build)) } diff --git a/google/resource_cloudbuild_build_trigger_test.go b/google/resource_cloudbuild_build_trigger_test.go index 63fefd9af3e..c9d9bdf0e25 100644 --- a/google/resource_cloudbuild_build_trigger_test.go +++ b/google/resource_cloudbuild_build_trigger_test.go @@ -7,6 +7,7 @@ import ( "github.com/hashicorp/terraform/helper/acctest" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" + cloudbuild "google.golang.org/api/cloudbuild/v1" ) func TestAccCloudBuildTrigger_basic(t *testing.T) { @@ -37,24 +38,80 @@ func TestAccCloudBuildTrigger_basic(t *testing.T) { }) } +func TestAccCloudBuildTrigger_filename(t *testing.T) { + t.Parallel() + + projectID := "terraform-" + acctest.RandString(10) + projectOrg := getTestOrgFromEnv(t) + projectBillingAccount := getTestBillingAccountFromEnv(t) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckGoogleCloudBuildTriggerVersionsDestroyed, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testGoogleCloudBuildTrigger_filename(projectID, projectOrg, projectBillingAccount), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleCloudFilenameConfig("google_cloudbuild_trigger.filename_build_trigger"), + ), + }, + resource.TestStep{ + Config: testGoogleCloudBuildTrigger_removed(projectID, projectOrg, projectBillingAccount), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleCloudBuildTriggerWasRemovedFromState("google_cloudbuild_trigger.filename_build_trigger"), + ), + }, + }, + }) + +} + +func testAccGetBuildTrigger(s *terraform.State, resourceName string) (*cloudbuild.BuildTrigger, error) { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return nil, fmt.Errorf("Resource not found: %s", resourceName) + } + + if rs.Primary.ID == "" { + return nil, fmt.Errorf("No ID is set") + } + + config := testAccProvider.Meta().(*Config) + project := rs.Primary.Attributes["project"] + + trigger, err := config.clientBuild.Projects.Triggers.Get(project, rs.Primary.ID).Do() + if err != nil { + return nil, fmt.Errorf("Trigger does not exist") + } + + return trigger, nil +} + func testAccCheckGoogleCloudBuildTriggerExists(resourceName string) resource.TestCheckFunc { return func(s *terraform.State) error { + _, err := testAccGetBuildTrigger(s, resourceName) - rs, ok := s.RootModule().Resources[resourceName] - if !ok { - return fmt.Errorf("Resource not found: %s", resourceName) + if err != nil { + return fmt.Errorf("Trigger does not exist") } - if rs.Primary.ID == "" { - return fmt.Errorf("No ID is set") - } - config := testAccProvider.Meta().(*Config) - project := rs.Primary.Attributes["project"] + return nil + } +} + +func testAccCheckGoogleCloudFilenameConfig(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + trigger, err := testAccGetBuildTrigger(s, resourceName) - _, err := config.clientBuild.Projects.Triggers.Get(project, rs.Primary.ID).Do() if err != nil { return fmt.Errorf("Trigger does not exist") } + + if trigger.Filename != "cloudbuild.yaml" { + return fmt.Errorf("Config filename mismatch: %s", trigger.Filename) + } + return nil } } @@ -147,6 +204,40 @@ resource "google_cloudbuild_trigger" "build_trigger" { `, projectID, projectID, projectOrg, projectBillingAccount) } +func testGoogleCloudBuildTrigger_filename(projectID, projectOrg, projectBillingAccount string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + name = "%s" + project_id = "%s" + org_id = "%s" + billing_account = "%s" +} + +resource "google_project_services" "acceptance" { + project = "${google_project.acceptance.project_id}" + + services = [ + "cloudbuild.googleapis.com", + "containerregistry.googleapis.com", + "logging.googleapis.com", + "pubsub.googleapis.com", + "storage-api.googleapis.com", + ] +} + +resource "google_cloudbuild_trigger" "filename_build_trigger" { + project = "${google_project_services.acceptance.project}" + description = "acceptance test build trigger" + trigger_template { + branch_name = "master" + project = "${google_project_services.acceptance.project}" + repo_name = "some-repo" + } + filename = "cloudbuild.yaml" +} + `, projectID, projectID, projectOrg, projectBillingAccount) +} + func testGoogleCloudBuildTrigger_removed(projectID, projectOrg, projectBillingAccount string) string { return fmt.Sprintf(` resource "google_project" "acceptance" { diff --git a/google/resource_compute_address.go b/google/resource_compute_address.go index b0c2bd3f990..170177cbb94 100644 --- a/google/resource_compute_address.go +++ b/google/resource_compute_address.go @@ -7,7 +7,7 @@ import ( "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/validation" - "google.golang.org/api/compute/v1" + computeBeta "google.golang.org/api/compute/v0.beta" ) const ( @@ -65,6 +65,14 @@ func resourceComputeAddress() *schema.Resource { Computed: true, }, + "network_tier": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + ValidateFunc: validation.StringInSlice([]string{"PREMIUM", "STANDARD"}, false), + }, + "project": &schema.Schema{ Type: schema.TypeString, Optional: true, @@ -101,14 +109,15 @@ func resourceComputeAddressCreate(d *schema.ResourceData, meta interface{}) erro } // Build the address parameter - address := &compute.Address{ + address := &computeBeta.Address{ Name: d.Get("name").(string), AddressType: d.Get("address_type").(string), Subnetwork: d.Get("subnetwork").(string), Address: d.Get("address").(string), + NetworkTier: d.Get("network_tier").(string), } - op, err := config.clientCompute.Addresses.Insert(project, region, address).Do() + op, err := config.clientComputeBeta.Addresses.Insert(project, region, address).Do() if err != nil { return fmt.Errorf("Error creating address: %s", err) } @@ -136,7 +145,7 @@ func resourceComputeAddressRead(d *schema.ResourceData, meta interface{}) error return err } - addr, err := config.clientCompute.Addresses.Get( + addr, err := config.clientComputeBeta.Addresses.Get( addressId.Project, addressId.Region, addressId.Name).Do() if err != nil { return handleNotFoundError(err, d, fmt.Sprintf("Address %q", d.Get("name").(string))) @@ -147,10 +156,11 @@ func resourceComputeAddressRead(d *schema.ResourceData, meta interface{}) error if addr.AddressType == "" { d.Set("address_type", addressTypeExternal) } - d.Set("subnetwork", addr.Subnetwork) + d.Set("subnetwork", ConvertSelfLinkToV1(addr.Subnetwork)) d.Set("address", addr.Address) - d.Set("self_link", addr.SelfLink) + d.Set("self_link", ConvertSelfLinkToV1(addr.SelfLink)) d.Set("name", addr.Name) + d.Set("network_tier", addr.NetworkTier) d.Set("project", addressId.Project) d.Set("region", GetResourceNameFromSelfLink(addr.Region)) @@ -166,7 +176,7 @@ func resourceComputeAddressDelete(d *schema.ResourceData, meta interface{}) erro } // Delete the address - op, err := config.clientCompute.Addresses.Delete( + op, err := config.clientComputeBeta.Addresses.Delete( addressId.Project, addressId.Region, addressId.Name).Do() if err != nil { return fmt.Errorf("Error deleting address: %s", err) diff --git a/google/resource_compute_address_test.go b/google/resource_compute_address_test.go index 806450d40f3..ed17ad933fb 100644 --- a/google/resource_compute_address_test.go +++ b/google/resource_compute_address_test.go @@ -101,6 +101,26 @@ func TestAccComputeAddress_basic(t *testing.T) { }) } +func TestAccComputeAddress_networkTier(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeAddressDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeAddress_networkTier(acctest.RandString(10)), + }, + resource.TestStep{ + ResourceName: "google_compute_address.foobar", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func TestAccComputeAddress_internal(t *testing.T) { var addr computeBeta.Address @@ -273,3 +293,11 @@ resource "google_compute_address" "internal_with_subnet_and_address" { i, // google_compute_address.internal_with_subnet_and_address name ) } + +func testAccComputeAddress_networkTier(i string) string { + return fmt.Sprintf(` +resource "google_compute_address" "foobar" { + name = "address-test-%s" + network_tier = "STANDARD" +}`, i) +} diff --git a/google/resource_compute_disk.go b/google/resource_compute_disk.go index 2580b0f2d99..140686cc34b 100644 --- a/google/resource_compute_disk.go +++ b/google/resource_compute_disk.go @@ -236,10 +236,10 @@ func diskEncryptionKeyDiffSuppress(k, old, new string, d *schema.ResourceData) b } } else if strings.HasSuffix(k, "raw_key") { disk_key := d.Get("disk_encryption_key_raw").(string) - return disk_key == old + return disk_key == old && old != "" && new == "" } else if k == "disk_encryption_key_raw" { disk_key := d.Get("disk_encryption_key.0.raw_key").(string) - return disk_key == old + return disk_key == old && old != "" && new == "" } return false } @@ -1004,7 +1004,7 @@ func expandComputeDiskDiskEncryptionKey(v interface{}, d *schema.ResourceData, c req = append(req, outMap) } else { // Check alternative setting? - if altV, ok := d.GetOk("disk_encryption_key_raw"); ok { + if altV, ok := d.GetOk("disk_encryption_key_raw"); ok && altV != "" { outMap := make(map[string]interface{}) outMap["rawKey"] = altV req = append(req, outMap) diff --git a/google/resource_compute_disk_test.go b/google/resource_compute_disk_test.go index b55254596f8..5ae0f430740 100644 --- a/google/resource_compute_disk_test.go +++ b/google/resource_compute_disk_test.go @@ -566,9 +566,9 @@ func testAccCheckEncryptionKey(n string, disk *compute.Disk) resource.TestCheckF } attr := rs.Primary.Attributes["disk_encryption_key_sha256"] - if disk.DiskEncryptionKey == nil && attr != "" { + if disk.DiskEncryptionKey == nil { return fmt.Errorf("Disk %s has mismatched encryption key.\nTF State: %+v\nGCP State: ", n, attr) - } else if disk.DiskEncryptionKey != nil && attr != disk.DiskEncryptionKey.Sha256 { + } else if attr != disk.DiskEncryptionKey.Sha256 { return fmt.Errorf("Disk %s has mismatched encryption key.\nTF State: %+v.\nGCP State: %+v", n, attr, disk.DiskEncryptionKey.Sha256) } diff --git a/google/resource_compute_forwarding_rule.go b/google/resource_compute_forwarding_rule.go index bf352d187b0..0bd1fb6d8b5 100644 --- a/google/resource_compute_forwarding_rule.go +++ b/google/resource_compute_forwarding_rule.go @@ -5,6 +5,8 @@ import ( "log" "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" + computeBeta "google.golang.org/api/compute/v0.beta" "google.golang.org/api/compute/v1" ) @@ -26,8 +28,9 @@ func resourceComputeForwardingRule() *schema.Resource { }, "target": &schema.Schema{ - Type: schema.TypeString, - Optional: true, + Type: schema.TypeString, + Optional: true, + DiffSuppressFunc: compareSelfLinkRelativePaths, }, "backend_service": &schema.Schema{ @@ -72,6 +75,14 @@ func resourceComputeForwardingRule() *schema.Resource { DiffSuppressFunc: compareSelfLinkOrResourceName, }, + "network_tier": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + ValidateFunc: validation.StringInSlice([]string{"PREMIUM", "STANDARD"}, false), + }, + "port_range": &schema.Schema{ Type: schema.TypeString, Optional: true, @@ -147,7 +158,7 @@ func resourceComputeForwardingRuleCreate(d *schema.ResourceData, meta interface{ ports = append(ports, v.(string)) } - frule := &compute.ForwardingRule{ + frule := &computeBeta.ForwardingRule{ BackendService: d.Get("backend_service").(string), IPAddress: d.Get("ip_address").(string), IPProtocol: d.Get("ip_protocol").(string), @@ -155,6 +166,7 @@ func resourceComputeForwardingRuleCreate(d *schema.ResourceData, meta interface{ LoadBalancingScheme: d.Get("load_balancing_scheme").(string), Name: d.Get("name").(string), Network: network.RelativeLink(), + NetworkTier: d.Get("network_tier").(string), PortRange: d.Get("port_range").(string), Ports: ports, Subnetwork: subnetwork.RelativeLink(), @@ -162,7 +174,7 @@ func resourceComputeForwardingRuleCreate(d *schema.ResourceData, meta interface{ } log.Printf("[DEBUG] ForwardingRule insert request: %#v", frule) - op, err := config.clientCompute.ForwardingRules.Insert( + op, err := config.clientComputeBeta.ForwardingRules.Insert( project, region, frule).Do() if err != nil { return fmt.Errorf("Error creating ForwardingRule: %s", err) @@ -171,7 +183,7 @@ func resourceComputeForwardingRuleCreate(d *schema.ResourceData, meta interface{ // It probably maybe worked, so store the ID now d.SetId(frule.Name) - err = computeOperationWait(config.clientCompute, op, project, "Creating Fowarding Rule") + err = computeSharedOperationWait(config.clientCompute, op, project, "Creating Fowarding Rule") if err != nil { return err } @@ -229,18 +241,19 @@ func resourceComputeForwardingRuleRead(d *schema.ResourceData, meta interface{}) return err } - frule, err := config.clientCompute.ForwardingRules.Get( + frule, err := config.clientComputeBeta.ForwardingRules.Get( project, region, d.Id()).Do() if err != nil { return handleNotFoundError(err, d, fmt.Sprintf("Forwarding Rule %q", d.Get("name").(string))) } d.Set("name", frule.Name) - d.Set("target", frule.Target) - d.Set("backend_service", frule.BackendService) + d.Set("target", ConvertSelfLinkToV1(frule.Target)) + d.Set("backend_service", ConvertSelfLinkToV1(frule.BackendService)) d.Set("description", frule.Description) d.Set("load_balancing_scheme", frule.LoadBalancingScheme) d.Set("network", frule.Network) + d.Set("network_tier", frule.NetworkTier) d.Set("port_range", frule.PortRange) d.Set("ports", frule.Ports) d.Set("project", project) @@ -248,7 +261,7 @@ func resourceComputeForwardingRuleRead(d *schema.ResourceData, meta interface{}) d.Set("subnetwork", frule.Subnetwork) d.Set("ip_address", frule.IPAddress) d.Set("ip_protocol", frule.IPProtocol) - d.Set("self_link", frule.SelfLink) + d.Set("self_link", ConvertSelfLinkToV1(frule.SelfLink)) return nil } diff --git a/google/resource_compute_forwarding_rule_test.go b/google/resource_compute_forwarding_rule_test.go index f8065061bd6..3698aee0f6a 100644 --- a/google/resource_compute_forwarding_rule_test.go +++ b/google/resource_compute_forwarding_rule_test.go @@ -118,6 +118,30 @@ func TestAccComputeForwardingRule_internalLoadBalancing(t *testing.T) { }) } +func TestAccComputeForwardingRule_networkTier(t *testing.T) { + t.Parallel() + + poolName := fmt.Sprintf("tf-%s", acctest.RandString(10)) + ruleName := fmt.Sprintf("tf-%s", acctest.RandString(10)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeForwardingRuleDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeForwardingRule_networkTier(poolName, ruleName), + }, + + resource.TestStep{ + ResourceName: "google_compute_forwarding_rule.foobar", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func testAccCheckComputeForwardingRuleDestroy(s *terraform.State) error { config := testAccProvider.Meta().(*Config) @@ -254,3 +278,22 @@ resource "google_compute_forwarding_rule" "foobar2" { } `, serviceName, checkName, networkName, ruleName1, networkName, ruleName2) } + +func testAccComputeForwardingRule_networkTier(poolName, ruleName string) string { + return fmt.Sprintf(` +resource "google_compute_target_pool" "foobar-tp" { + description = "Resource created for Terraform acceptance testing" + instances = ["us-central1-a/foo", "us-central1-b/bar"] + name = "%s" +} +resource "google_compute_forwarding_rule" "foobar" { + description = "Resource created for Terraform acceptance testing" + ip_protocol = "UDP" + name = "%s" + port_range = "80-81" + target = "${google_compute_target_pool.foobar-tp.self_link}" + + network_tier = "STANDARD" +} +`, poolName, ruleName) +} diff --git a/google/resource_compute_instance.go b/google/resource_compute_instance.go index 325a2c1fbf3..18373fafb0a 100644 --- a/google/resource_compute_instance.go +++ b/google/resource_compute_instance.go @@ -355,6 +355,13 @@ func resourceComputeInstance() *schema.Resource { Computed: true, }, + "network_tier": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringInSlice([]string{"PREMIUM", "STANDARD"}, false), + }, + // It's unclear why this field exists, as // nat_ip can be both optional and computed. // Consider deprecating it. @@ -1096,20 +1103,22 @@ func resourceComputeInstanceUpdate(d *schema.ResourceData, meta interface{}) err accessConfigsCount := d.Get(prefix + ".access_config.#").(int) for j := 0; j < accessConfigsCount; j++ { acPrefix := fmt.Sprintf("%s.access_config.%d", prefix, j) - ac := &compute.AccessConfig{ - Type: "ONE_TO_ONE_NAT", - NatIP: d.Get(acPrefix + ".nat_ip").(string), + ac := &computeBeta.AccessConfig{ + Type: "ONE_TO_ONE_NAT", + NatIP: d.Get(acPrefix + ".nat_ip").(string), + NetworkTier: d.Get(acPrefix + ".network_tier").(string), } if ptr, ok := d.GetOk(acPrefix + ".public_ptr_domain_name"); ok && ptr != "" { ac.SetPublicPtr = true ac.PublicPtrDomainName = ptr.(string) } - op, err := config.clientCompute.Instances.AddAccessConfig( + + op, err := config.clientComputeBeta.Instances.AddAccessConfig( project, zone, d.Id(), networkName, ac).Do() if err != nil { return fmt.Errorf("Error adding new access_config: %s", err) } - opErr := computeOperationWaitTime(config.clientCompute, op, project, "new access_config to add", int(d.Timeout(schema.TimeoutUpdate).Minutes())) + opErr := computeSharedOperationWaitTime(config.clientCompute, op, project, int(d.Timeout(schema.TimeoutUpdate).Minutes()), "new access_config to add") if opErr != nil { return opErr } diff --git a/google/resource_compute_instance_group_manager.go b/google/resource_compute_instance_group_manager.go index 3310d8ac670..c06840edc57 100644 --- a/google/resource_compute_instance_group_manager.go +++ b/google/resource_compute_instance_group_manager.go @@ -33,10 +33,50 @@ func resourceComputeInstanceGroupManager() *schema.Resource { "instance_template": &schema.Schema{ Type: schema.TypeString, - Required: true, + Optional: true, DiffSuppressFunc: compareSelfLinkRelativePaths, }, + "version": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "instance_template": &schema.Schema{ + Type: schema.TypeString, + Required: true, + DiffSuppressFunc: compareSelfLinkRelativePaths, + }, + + "target_size": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "fixed": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + }, + + "percent": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + ValidateFunc: validation.IntBetween(0, 100), + }, + }, + }, + }, + }, + }, + }, + "name": &schema.Schema{ Type: schema.TypeString, Required: true, @@ -138,6 +178,7 @@ func resourceComputeInstanceGroupManager() *schema.Resource { }, }, }, + "rolling_update_policy": &schema.Schema{ Type: schema.TypeList, Optional: true, @@ -192,6 +233,7 @@ func resourceComputeInstanceGroupManager() *schema.Resource { }, }, }, + "wait_for_instances": &schema.Schema{ Type: schema.TypeBool, Optional: true, @@ -254,6 +296,7 @@ func resourceComputeInstanceGroupManagerCreate(d *schema.ResourceData, meta inte NamedPorts: getNamedPortsBeta(d.Get("named_port").([]interface{})), TargetPools: convertStringSet(d.Get("target_pools").(*schema.Set)), AutoHealingPolicies: expandAutoHealingPolicies(d.Get("auto_healing_policies").([]interface{})), + Versions: expandVersions(d.Get("version").([]interface{})), // Force send TargetSize to allow a value of 0. ForceSendFields: []string{"TargetSize"}, } @@ -290,6 +333,31 @@ func flattenNamedPortsBeta(namedPorts []*computeBeta.NamedPort) []map[string]int } +func flattenVersions(versions []*computeBeta.InstanceGroupManagerVersion) []map[string]interface{} { + result := make([]map[string]interface{}, 0, len(versions)) + for _, version := range versions { + versionMap := make(map[string]interface{}) + versionMap["name"] = version.Name + versionMap["instance_template"] = ConvertSelfLinkToV1(version.InstanceTemplate) + versionMap["target_size"] = flattenFixedOrPercent(version.TargetSize) + result = append(result, versionMap) + } + + return result +} + +func flattenFixedOrPercent(fixedOrPercent *computeBeta.FixedOrPercent) []map[string]interface{} { + result := make(map[string]interface{}) + if value := fixedOrPercent.Percent; value > 0 { + result["percent"] = value + } else if value := fixedOrPercent.Fixed; value > 0 { + result["fixed"] = fixedOrPercent.Fixed + } else { + return []map[string]interface{}{} + } + return []map[string]interface{}{result} +} + func getManager(d *schema.ResourceData, meta interface{}) (*computeBeta.InstanceGroupManager, error) { config := meta.(*Config) @@ -352,6 +420,9 @@ func resourceComputeInstanceGroupManagerRead(d *schema.ResourceData, meta interf d.Set("base_instance_name", manager.BaseInstanceName) d.Set("instance_template", ConvertSelfLinkToV1(manager.InstanceTemplate)) + if err := d.Set("version", flattenVersions(manager.Versions)); err != nil { + return err + } d.Set("name", manager.Name) d.Set("zone", GetResourceNameFromSelfLink(manager.Zone)) d.Set("description", manager.Description) @@ -385,6 +456,63 @@ func resourceComputeInstanceGroupManagerRead(d *schema.ResourceData, meta interf return nil } +// Updates an instance group manager by applying an update strategy (REPLACE, RESTART) respecting a rolling update policy (availability settings, +// interval between updates, and particularly, the type of update PROACTIVE or OPPORTUNISTIC because updates performed by API are considered +// OPPORTUNISTIC by default) +func performUpdate(config *Config, id string, updateStrategy string, rollingUpdatePolicy *computeBeta.InstanceGroupManagerUpdatePolicy, versions []*computeBeta.InstanceGroupManagerVersion, project string, zone string) error { + if updateStrategy == "RESTART" { + managedInstances, err := config.clientComputeBeta.InstanceGroupManagers.ListManagedInstances(project, zone, id).Do() + if err != nil { + return fmt.Errorf("Error getting instance group managers instances: %s", err) + } + + managedInstanceCount := len(managedInstances.ManagedInstances) + instances := make([]string, managedInstanceCount) + for i, v := range managedInstances.ManagedInstances { + instances[i] = v.Instance + } + + recreateInstances := &computeBeta.InstanceGroupManagersRecreateInstancesRequest{ + Instances: instances, + } + + op, err := config.clientComputeBeta.InstanceGroupManagers.RecreateInstances(project, zone, id, recreateInstances).Do() + if err != nil { + return fmt.Errorf("Error restarting instance group managers instances: %s", err) + } + + // Wait for the operation to complete + err = computeSharedOperationWaitTime(config.clientCompute, op, project, managedInstanceCount*4, "Restarting InstanceGroupManagers instances") + if err != nil { + return err + } + } + + if updateStrategy == "ROLLING_UPDATE" { + // UpdatePolicy is set for InstanceGroupManager on update only, because it is only relevant for `Patch` calls. + // Other tools(gcloud and UI) capable of executing the same `ROLLING UPDATE` call + // expect those values to be provided by user as part of the call + // or provide their own defaults without respecting what was previously set on UpdateManager. + // To follow the same logic, we provide policy values on relevant update change only. + manager := &computeBeta.InstanceGroupManager{ + UpdatePolicy: rollingUpdatePolicy, + Versions: versions, + } + + op, err := config.clientComputeBeta.InstanceGroupManagers.Patch(project, zone, id, manager).Do() + if err != nil { + return fmt.Errorf("Error updating managed group instances: %s", err) + } + + err = computeSharedOperationWait(config.clientCompute, op, project, "Updating managed group instances") + if err != nil { + return err + } + } + + return nil +} + func resourceComputeInstanceGroupManagerUpdate(d *schema.ResourceData, meta interface{}) error { config := meta.(*Config) @@ -430,81 +558,6 @@ func resourceComputeInstanceGroupManagerUpdate(d *schema.ResourceData, meta inte d.SetPartial("target_pools") } - // If instance_template changes then update - if d.HasChange("instance_template") { - // Build the parameter - setInstanceTemplate := &computeBeta.InstanceGroupManagersSetInstanceTemplateRequest{ - InstanceTemplate: d.Get("instance_template").(string), - } - - op, err := config.clientComputeBeta.InstanceGroupManagers.SetInstanceTemplate( - project, zone, d.Id(), setInstanceTemplate).Do() - - if err != nil { - return fmt.Errorf("Error updating InstanceGroupManager: %s", err) - } - - // Wait for the operation to complete - err = computeSharedOperationWait(config.clientCompute, op, project, "Updating InstanceGroupManager") - if err != nil { - return err - } - - if d.Get("update_strategy").(string) == "RESTART" { - managedInstances, err := config.clientComputeBeta.InstanceGroupManagers.ListManagedInstances( - project, zone, d.Id()).Do() - if err != nil { - return fmt.Errorf("Error getting instance group managers instances: %s", err) - } - - managedInstanceCount := len(managedInstances.ManagedInstances) - instances := make([]string, managedInstanceCount) - for i, v := range managedInstances.ManagedInstances { - instances[i] = v.Instance - } - - recreateInstances := &computeBeta.InstanceGroupManagersRecreateInstancesRequest{ - Instances: instances, - } - - op, err = config.clientComputeBeta.InstanceGroupManagers.RecreateInstances( - project, zone, d.Id(), recreateInstances).Do() - if err != nil { - return fmt.Errorf("Error restarting instance group managers instances: %s", err) - } - - // Wait for the operation to complete - err = computeSharedOperationWaitTime(config.clientCompute, op, project, managedInstanceCount*4, "Restarting InstanceGroupManagers instances") - if err != nil { - return err - } - } - - if d.Get("update_strategy").(string) == "ROLLING_UPDATE" { - // UpdatePolicy is set for InstanceGroupManager on update only, because it is only relevant for `Patch` calls. - // Other tools(gcloud and UI) capable of executing the same `ROLLING UPDATE` call - // expect those values to be provided by user as part of the call - // or provide their own defaults without respecting what was previously set on UpdateManager. - // To follow the same logic, we provide policy values on relevant update change only. - manager := &computeBeta.InstanceGroupManager{ - UpdatePolicy: expandUpdatePolicy(d.Get("rolling_update_policy").([]interface{})), - } - - op, err = config.clientComputeBeta.InstanceGroupManagers.Patch( - project, zone, d.Id(), manager).Do() - if err != nil { - return fmt.Errorf("Error updating managed group instances: %s", err) - } - - err = computeSharedOperationWait(config.clientCompute, op, project, "Updating managed group instances") - if err != nil { - return err - } - } - - d.SetPartial("instance_template") - } - // If named_port changes then update: if d.HasChange("named_port") { @@ -572,6 +625,44 @@ func resourceComputeInstanceGroupManagerUpdate(d *schema.ResourceData, meta inte d.SetPartial("auto_healing_policies") } + // If instance_template changes then update + if d.HasChange("instance_template") { + // Build the parameter + setInstanceTemplate := &computeBeta.InstanceGroupManagersSetInstanceTemplateRequest{ + InstanceTemplate: d.Get("instance_template").(string), + } + + op, err := config.clientComputeBeta.InstanceGroupManagers.SetInstanceTemplate(project, zone, d.Id(), setInstanceTemplate).Do() + + if err != nil { + return fmt.Errorf("Error updating InstanceGroupManager: %s", err) + } + + // Wait for the operation to complete + err = computeSharedOperationWait(config.clientCompute, op, project, "Updating InstanceGroupManager") + if err != nil { + return err + } + + updateStrategy := d.Get("update_strategy").(string) + rollingUpdatePolicy := expandUpdatePolicy(d.Get("rolling_update_policy").([]interface{})) + err = performUpdate(config, d.Id(), updateStrategy, rollingUpdatePolicy, nil, project, zone) + d.SetPartial("instance_template") + } + + // If version changes then update + if d.HasChange("version") { + updateStrategy := d.Get("update_strategy").(string) + rollingUpdatePolicy := expandUpdatePolicy(d.Get("rolling_update_policy").([]interface{})) + versions := expandVersions(d.Get("version").([]interface{})) + err = performUpdate(config, d.Id(), updateStrategy, rollingUpdatePolicy, versions, project, zone) + if err != nil { + return err + } + + d.SetPartial("version") + } + d.Partial(false) return resourceComputeInstanceGroupManagerRead(d, meta) @@ -647,6 +738,37 @@ func expandAutoHealingPolicies(configured []interface{}) []*computeBeta.Instance return autoHealingPolicies } +func expandVersions(configured []interface{}) []*computeBeta.InstanceGroupManagerVersion { + versions := make([]*computeBeta.InstanceGroupManagerVersion, 0, len(configured)) + for _, raw := range configured { + data := raw.(map[string]interface{}) + + version := computeBeta.InstanceGroupManagerVersion{ + Name: data["name"].(string), + InstanceTemplate: data["instance_template"].(string), + TargetSize: expandFixedOrPercent(data["target_size"].([]interface{})), + } + + versions = append(versions, &version) + } + return versions +} + +func expandFixedOrPercent(configured []interface{}) *computeBeta.FixedOrPercent { + fixedOrPercent := &computeBeta.FixedOrPercent{} + + for _, raw := range configured { + data := raw.(map[string]interface{}) + if percent := data["percent"]; percent.(int) > 0 { + fixedOrPercent.Percent = int64(percent.(int)) + } else { + fixedOrPercent.Fixed = int64(data["fixed"].(int)) + fixedOrPercent.ForceSendFields = []string{"Fixed"} + } + } + return fixedOrPercent +} + func expandUpdatePolicy(configured []interface{}) *computeBeta.InstanceGroupManagerUpdatePolicy { updatePolicy := &computeBeta.InstanceGroupManagerUpdatePolicy{} diff --git a/google/resource_compute_instance_group_manager_test.go b/google/resource_compute_instance_group_manager_test.go index b7fb247a758..03871be60b8 100644 --- a/google/resource_compute_instance_group_manager_test.go +++ b/google/resource_compute_instance_group_manager_test.go @@ -269,6 +269,36 @@ func TestAccInstanceGroupManager_separateRegions(t *testing.T) { }) } +func TestAccInstanceGroupManager_versions(t *testing.T) { + t.Parallel() + + var manager computeBeta.InstanceGroupManager + + primaryTemplate := fmt.Sprintf("igm-test-%s", acctest.RandString(10)) + canaryTemplate := fmt.Sprintf("igm-test-%s", acctest.RandString(10)) + igm := 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_versions(primaryTemplate, canaryTemplate, igm), + Check: resource.ComposeTestCheckFunc( + testAccCheckInstanceGroupManagerBetaExists("google_compute_instance_group_manager.igm-basic", &manager), + testAccCheckInstanceGroupManagerVersions("google_compute_instance_group_manager.igm-basic", primaryTemplate, canaryTemplate), + ), + }, + resource.TestStep{ + ResourceName: "google_compute_instance_group_manager.igm-basic", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func TestAccInstanceGroupManager_autoHealingPolicies(t *testing.T) { t.Parallel() @@ -492,6 +522,42 @@ func testAccCheckInstanceGroupManagerNamedPorts(n string, np map[string]int64, i } } +func testAccCheckInstanceGroupManagerVersions(n string, primaryTemplate string, canaryTemplate 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) + + manager, err := config.clientComputeBeta.InstanceGroupManagers.Get(config.Project, rs.Primary.Attributes["zone"], rs.Primary.ID).Do() + if err != nil { + return err + } + + if len(manager.Versions) != 2 { + return fmt.Errorf("Expected # of versions to be 2, got %d", len(manager.Versions)) + } + + primaryVersion := manager.Versions[0] + if !strings.Contains(primaryVersion.InstanceTemplate, primaryTemplate) { + return fmt.Errorf("Expected string \"%s\" to appear in \"%s\"", primaryTemplate, primaryVersion.InstanceTemplate) + } + + canaryVersion := manager.Versions[1] + if !strings.Contains(canaryVersion.InstanceTemplate, canaryTemplate) { + return fmt.Errorf("Expected string \"%s\" to appear in \"%s\"", canaryTemplate, canaryVersion.InstanceTemplate) + } + + return nil + } +} + func testAccCheckInstanceGroupManagerAutoHealingPolicies(n, hck string, initialDelaySec int64) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] @@ -1121,6 +1187,73 @@ resource "google_compute_http_health_check" "zero" { `, template, target, igm, hck) } +func testAccInstanceGroupManager_versions(primaryTemplate string, canaryTemplate string, igm string) string { + return fmt.Sprintf(` +resource "google_compute_instance_template" "igm-primary" { + name = "%s" + 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_template" "igm-canary" { + name = "%s" + 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" + base_instance_name = "igm-basic" + zone = "us-central1-c" + target_size = 2 + + version { + name = "primary" + instance_template = "${google_compute_instance_template.igm-primary.self_link}" + } + + version { + name = "canary" + instance_template = "${google_compute_instance_template.igm-canary.self_link}" + target_size { + fixed = 1 + } + } +} + `, primaryTemplate, canaryTemplate, igm) +} + // This test is to make sure that a single version resource can link to a versioned resource // without perpetual diffs because the self links mismatch. // Once auto_healing_policies is no longer beta, we will need to use a new field or resource diff --git a/google/resource_compute_instance_template.go b/google/resource_compute_instance_template.go index 89b4af9e36e..c21194db09d 100644 --- a/google/resource_compute_instance_template.go +++ b/google/resource_compute_instance_template.go @@ -5,6 +5,7 @@ import ( "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" computeBeta "google.golang.org/api/compute/v0.beta" "google.golang.org/api/googleapi" ) @@ -237,6 +238,12 @@ func resourceComputeInstanceTemplate() *schema.Resource { ForceNew: true, Computed: true, }, + "network_tier": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringInSlice([]string{"PREMIUM", "STANDARD"}, false), + }, // Instance templates will never have an // 'assigned NAT IP', but we need this in // the schema to allow us to share flatten diff --git a/google/resource_compute_instance_template_test.go b/google/resource_compute_instance_template_test.go index d622de974db..f2b09f7ed02 100644 --- a/google/resource_compute_instance_template_test.go +++ b/google/resource_compute_instance_template_test.go @@ -99,6 +99,26 @@ func TestAccComputeInstanceTemplate_IP(t *testing.T) { }) } +func TestAccComputeInstanceTemplate_networkTier(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeInstanceTemplateDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeInstanceTemplate_networkTier(), + }, + resource.TestStep{ + ResourceName: "google_compute_instance_template.foobar", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func TestAccComputeInstanceTemplate_networkIP(t *testing.T) { t.Parallel() @@ -804,6 +824,25 @@ resource "google_compute_instance_template" "foobar" { }`, acctest.RandString(10), acctest.RandString(10)) } +func testAccComputeInstanceTemplate_networkTier() string { + return fmt.Sprintf(` +resource "google_compute_instance_template" "foobar" { + name = "instancet-test-%s" + machine_type = "n1-standard-1" + + disk { + source_image = "debian-8-jessie-v20160803" + } + + network_interface { + network = "default" + access_config { + network_tier = "STANDARD" + } + } +}`, acctest.RandString(10)) +} + func testAccComputeInstanceTemplate_networkIP(networkIP string) string { return fmt.Sprintf(` resource "google_compute_instance_template" "foobar" { diff --git a/google/resource_compute_instance_test.go b/google/resource_compute_instance_test.go index b05de75a3e0..d24708d5c06 100644 --- a/google/resource_compute_instance_test.go +++ b/google/resource_compute_instance_test.go @@ -243,6 +243,29 @@ func TestAccComputeInstance_GenerateIP(t *testing.T) { }) } +func TestAccComputeInstance_networkTier(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_networkTier(instanceName), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists( + "google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceAccessConfigHasIP(&instance), + testAccCheckComputeInstanceHasAssignedIP, + ), + }, + computeInstanceImportStep("us-central1-a", instanceName, []string{}), + }, + }) +} + func TestAccComputeInstance_diskEncryption(t *testing.T) { t.Parallel() @@ -1855,6 +1878,29 @@ resource "google_compute_instance" "foobar" { `, instance) } +func testAccComputeInstance_networkTier(instance string) string { + return fmt.Sprintf(` +resource "google_compute_instance" "foobar" { + name = "%s" + machine_type = "n1-standard-1" + zone = "us-central1-a" + + boot_disk { + initialize_params{ + image = "debian-8-jessie-v20160803" + } + } + + network_interface { + network = "default" + access_config { + network_tier = "STANDARD" + } + } +} +`, instance) +} + func testAccComputeInstance_disks_encryption(bootEncryptionKey string, diskNameToEncryptionKey map[string]*compute.CustomerEncryptionKey, instance string) string { diskNames := []string{} for k, _ := range diskNameToEncryptionKey { diff --git a/google/resource_container_cluster.go b/google/resource_container_cluster.go index 19c34f1fd0d..54adaaf4f96 100644 --- a/google/resource_container_cluster.go +++ b/google/resource_container_cluster.go @@ -219,7 +219,7 @@ func resourceContainerCluster() *schema.Resource { Type: schema.TypeString, Optional: true, Computed: true, - ValidateFunc: validation.StringInSlice([]string{"logging.googleapis.com", "none"}, false), + ValidateFunc: validation.StringInSlice([]string{"logging.googleapis.com", "logging.googleapis.com/kubernetes", "none"}, false), }, "maintenance_policy": { @@ -322,9 +322,10 @@ func resourceContainerCluster() *schema.Resource { }, "monitoring_service": { - Type: schema.TypeString, - Optional: true, - Computed: true, + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringInSlice([]string{"monitoring.googleapis.com", "monitoring.googleapis.com/kubernetes", "none"}, false), }, "network": { diff --git a/google/resource_redis_instance.go b/google/resource_redis_instance.go index 2fcaae1413a..f7e8f72eb27 100644 --- a/google/resource_redis_instance.go +++ b/google/resource_redis_instance.go @@ -18,6 +18,7 @@ import ( "fmt" "log" "strconv" + "strings" "time" "github.com/hashicorp/terraform/helper/schema" @@ -29,6 +30,7 @@ func resourceRedisInstance() *schema.Resource { return &schema.Resource{ Create: resourceRedisInstanceCreate, Read: resourceRedisInstanceRead, + Update: resourceRedisInstanceUpdate, Delete: resourceRedisInstanceDelete, Importer: &schema.ResourceImporter{ @@ -37,6 +39,7 @@ func resourceRedisInstance() *schema.Resource { Timeouts: &schema.ResourceTimeout{ Create: schema.DefaultTimeout(360 * time.Second), + Update: schema.DefaultTimeout(240 * time.Second), Delete: schema.DefaultTimeout(240 * time.Second), }, @@ -44,7 +47,6 @@ func resourceRedisInstance() *schema.Resource { "memory_size_gb": { Type: schema.TypeInt, Required: true, - ForceNew: true, }, "name": { Type: schema.TypeString, @@ -57,20 +59,19 @@ func resourceRedisInstance() *schema.Resource { ForceNew: true, }, "authorized_network": { - Type: schema.TypeString, - Computed: true, - Optional: true, - ForceNew: true, + Type: schema.TypeString, + Computed: true, + Optional: true, + ForceNew: true, + DiffSuppressFunc: compareSelfLinkRelativePaths, }, "display_name": { Type: schema.TypeString, Optional: true, - ForceNew: true, }, "labels": { Type: schema.TypeMap, Optional: true, - ForceNew: true, Elem: &schema.Schema{Type: schema.TypeString}, }, "location_id": { @@ -315,6 +316,119 @@ func resourceRedisInstanceRead(d *schema.ResourceData, meta interface{}) error { return nil } +func resourceRedisInstanceUpdate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + project, err := getProject(d, config) + if err != nil { + return err + } + + alternativeLocationIdProp, err := expandRedisInstanceAlternativeLocationId(d.Get("alternative_location_id"), d, config) + if err != nil { + return err + } + authorizedNetworkProp, err := expandRedisInstanceAuthorizedNetwork(d.Get("authorized_network"), d, config) + if err != nil { + return err + } + displayNameProp, err := expandRedisInstanceDisplayName(d.Get("display_name"), d, config) + if err != nil { + return err + } + labelsProp, err := expandRedisInstanceLabels(d.Get("labels"), d, config) + if err != nil { + return err + } + locationIdProp, err := expandRedisInstanceLocationId(d.Get("location_id"), d, config) + if err != nil { + return err + } + nameProp, err := expandRedisInstanceName(d.Get("name"), d, config) + if err != nil { + return err + } + memorySizeGbProp, err := expandRedisInstanceMemorySizeGb(d.Get("memory_size_gb"), d, config) + if err != nil { + return err + } + redisVersionProp, err := expandRedisInstanceRedisVersion(d.Get("redis_version"), d, config) + if err != nil { + return err + } + reservedIpRangeProp, err := expandRedisInstanceReservedIpRange(d.Get("reserved_ip_range"), d, config) + if err != nil { + return err + } + tierProp, err := expandRedisInstanceTier(d.Get("tier"), d, config) + if err != nil { + return err + } + regionProp, err := expandRedisInstanceRegion(d.Get("region"), d, config) + if err != nil { + return err + } + + obj := map[string]interface{}{ + "alternativeLocationId": alternativeLocationIdProp, + "authorizedNetwork": authorizedNetworkProp, + "displayName": displayNameProp, + "labels": labelsProp, + "locationId": locationIdProp, + "name": nameProp, + "memorySizeGb": memorySizeGbProp, + "redisVersion": redisVersionProp, + "reservedIpRange": reservedIpRangeProp, + "tier": tierProp, + "region": regionProp, + } + + obj, err = resourceRedisInstanceEncoder(d, meta, obj) + url, err := replaceVars(d, config, "https://redis.googleapis.com/v1beta1/projects/{{project}}/locations/{{region}}/instances/{{name}}") + if err != nil { + return err + } + + log.Printf("[DEBUG] Updating Instance %q: %#v", d.Id(), obj) + updateMask := []string{} + if d.HasChange("display_name") { + updateMask = append(updateMask, "displayName") + } + if d.HasChange("labels") { + updateMask = append(updateMask, "labels") + } + if d.HasChange("memory_size_gb") { + updateMask = append(updateMask, "memorySizeGb") + } + // updateMask is a URL parameter but not present in the schema, so replaceVars + // won't set it + url, err = addQueryParams(url, map[string]string{"updateMask": strings.Join(updateMask, ",")}) + if err != nil { + return err + } + res, err := sendRequest(config, "PATCH", url, obj) + + if err != nil { + return fmt.Errorf("Error updating Instance %q: %s", d.Id(), err) + } + + op := &redis.Operation{} + err = Convert(res, op) + if err != nil { + return err + } + + err = redisOperationWaitTime( + config.clientRedis, op, project, "Updating Instance", + int(d.Timeout(schema.TimeoutUpdate).Minutes())) + + if err != nil { + return err + } + + return resourceRedisInstanceRead(d, meta) +} + func resourceRedisInstanceDelete(d *schema.ResourceData, meta interface{}) error { config := meta.(*Config) @@ -443,7 +557,11 @@ func expandRedisInstanceAlternativeLocationId(v interface{}, d *schema.ResourceD } func expandRedisInstanceAuthorizedNetwork(v interface{}, d *schema.ResourceData, config *Config) (interface{}, error) { - return v, nil + fv, err := ParseNetworkFieldValue(v.(string), d, config) + if err != nil { + return nil, err + } + return fv.RelativeLink(), nil } func expandRedisInstanceDisplayName(v interface{}, d *schema.ResourceData, config *Config) (interface{}, error) { diff --git a/google/resource_redis_instance_test.go b/google/resource_redis_instance_test.go index 7d3bdefc8be..839563ed322 100644 --- a/google/resource_redis_instance_test.go +++ b/google/resource_redis_instance_test.go @@ -2,10 +2,12 @@ package google import ( "fmt" + "strings" "testing" "github.com/hashicorp/terraform/helper/acctest" "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" ) func TestAccRedisInstance_basic(t *testing.T) { @@ -16,7 +18,7 @@ func TestAccRedisInstance_basic(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, - CheckDestroy: testAccCheckComputeAddressDestroy, + CheckDestroy: testAccCheckRedisInstanceDestroy, Steps: []resource.TestStep{ resource.TestStep{ Config: testAccRedisInstance_basic(name), @@ -30,6 +32,36 @@ func TestAccRedisInstance_basic(t *testing.T) { }) } +func TestAccRedisInstance_update(t *testing.T) { + t.Parallel() + + name := acctest.RandomWithPrefix("tf-test") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckRedisInstanceDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccRedisInstance_update(name), + }, + resource.TestStep{ + ResourceName: "google_redis_instance.test", + ImportState: true, + ImportStateVerify: true, + }, + resource.TestStep{ + Config: testAccRedisInstance_update2(name), + }, + resource.TestStep{ + ResourceName: "google_redis_instance.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func TestAccRedisInstance_full(t *testing.T) { t.Parallel() @@ -39,7 +71,7 @@ func TestAccRedisInstance_full(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, - CheckDestroy: testAccCheckComputeAddressDestroy, + CheckDestroy: testAccCheckRedisInstanceDestroy, Steps: []resource.TestStep{ resource.TestStep{ Config: testAccRedisInstance_full(name, network), @@ -53,6 +85,31 @@ func TestAccRedisInstance_full(t *testing.T) { }) } +func testAccCheckRedisInstanceDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "google_redis_instance" { + continue + } + + redisIdParts := strings.Split(rs.Primary.ID, "/") + if len(redisIdParts) != 3 { + return fmt.Errorf("Unexpected resource ID %s, expected {project}/{region}/{name}", rs.Primary.ID) + } + + project, region, inst := redisIdParts[0], redisIdParts[1], redisIdParts[2] + + name := fmt.Sprintf("projects/%s/locations/%s/instances/%s", project, region, inst) + _, err := config.clientRedis.Projects.Locations.Get(name).Do() + if err == nil { + return fmt.Errorf("Redis instance still exists") + } + } + + return nil +} + func testAccRedisInstance_basic(name string) string { return fmt.Sprintf(` resource "google_redis_instance" "test" { @@ -61,6 +118,34 @@ resource "google_redis_instance" "test" { }`, name) } +func testAccRedisInstance_update(name string) string { + return fmt.Sprintf(` +resource "google_redis_instance" "test" { + name = "%s" + display_name = "pre-update" + memory_size_gb = 1 + + labels { + my_key = "my_val" + other_key = "other_val" + } +}`, name) +} + +func testAccRedisInstance_update2(name string) string { + return fmt.Sprintf(` +resource "google_redis_instance" "test" { + name = "%s" + display_name = "post-update" + memory_size_gb = 1 + + labels { + my_key = "my_val" + other_key = "new_val" + } +}`, name) +} + func testAccRedisInstance_full(name, network string) string { return fmt.Sprintf(` resource "google_compute_network" "test" { @@ -72,6 +157,8 @@ resource "google_redis_instance" "test" { tier = "STANDARD_HA" memory_size_gb = 1 + authorized_network = "${google_compute_network.test.self_link}" + region = "us-central1" location_id = "us-central1-a" alternative_location_id = "us-central1-f" @@ -84,5 +171,5 @@ resource "google_redis_instance" "test" { my_key = "my_val" other_key = "other_val" } -}`, name, network) +}`, network, name) } diff --git a/google/transport.go b/google/transport.go index 32a4765f334..ea931843ef2 100644 --- a/google/transport.go +++ b/google/transport.go @@ -106,15 +106,12 @@ func sendRequest(config *Config, method, rawurl string, body map[string]interfac } } - u, err := url.Parse(rawurl) + u, err := addQueryParams(rawurl, map[string]string{"alt": "json"}) if err != nil { return nil, err } - q := u.Query() - q.Set("alt", "json") - u.RawQuery = q.Encode() - req, err := http.NewRequest(method, u.String(), &buf) + req, err := http.NewRequest(method, u, &buf) if err != nil { return nil, err } @@ -136,6 +133,19 @@ func sendRequest(config *Config, method, rawurl string, body map[string]interfac return result, nil } +func addQueryParams(rawurl string, params map[string]string) (string, error) { + u, err := url.Parse(rawurl) + if err != nil { + return "", err + } + q := u.Query() + for k, v := range params { + q.Set(k, v) + } + u.RawQuery = q.Encode() + return u.String(), nil +} + func replaceVars(d TerraformResourceData, config *Config, linkTmpl string) (string, error) { re := regexp.MustCompile("{{([[:word:]]+)}}") var project, region, zone string diff --git a/website/docs/d/datasource_google_netblock_ip_ranges.html.markdown b/website/docs/d/datasource_google_netblock_ip_ranges.html.markdown new file mode 100644 index 00000000000..654642dca4e --- /dev/null +++ b/website/docs/d/datasource_google_netblock_ip_ranges.html.markdown @@ -0,0 +1,39 @@ +--- +layout: "google" +page_title: "Google: google_netblock_ip_ranges" +sidebar_current: "docs-google-datasource-netblock-ip-ranges" +description: |- + Use this data source to get the IP ranges from the sender policy framework (SPF) record of \_cloud-netblocks.googleusercontent.com +--- + +# google_netblock_ip_ranges + +Use this data source to get the IP ranges from the sender policy framework (SPF) record of \_cloud-netblocks.googleusercontent + +https://cloud.google.com/compute/docs/faq#where_can_i_find_product_name_short_ip_ranges + +## Example Usage + +```tf +data "google_netblock_ip_ranges" "netblock" {} + +output "cidr_blocks" { + value = "${data.google_netblock_ip_ranges.netblock.cidr_blocks}" +} + +output "cidr_blocks_ipv4" { + value = "${data.google_netblock_ip_ranges.netblock.cidr_blocks_ipv4}" +} + +output "cidr_blocks_ipv6" { + value = "${data.google_netblock_ip_ranges.netblock.cidr_blocks_ipv6}" +} +``` + +## Attributes Reference + +* `cidr_blocks` - Retrieve list of all CIDR blocks. + +* `cidr_blocks_ipv4` - Retrieve list of the IP4 CIDR blocks + +* `cidr_blocks_ipv6` - Retrieve list of the IP6 CIDR blocks. diff --git a/website/docs/d/google_compute_regions.html.markdown b/website/docs/d/google_compute_regions.html.markdown new file mode 100644 index 00000000000..85d7b9155ed --- /dev/null +++ b/website/docs/d/google_compute_regions.html.markdown @@ -0,0 +1,38 @@ +--- +layout: "google" +page_title: "Google: google_compute_regions" +sidebar_current: "docs-google-datasource-compute-regions" +description: |- + Provides a list of available Google Compute regions +--- + +# google\_compute\_regions + +Provides access to available Google Compute regions for a given project. +See more about [regions and regions](https://cloud.google.com/compute/docs/regions-zones/) in the upstream docs. + +``` +data "google_compute_regions" "available" {} + +resource "google_compute_subnetwork" "cluster" { + count = "${length(data.google_compute_regions.available.names)}" + name = "my-network" + ip_cidr_range = "10.36.${count.index}.0/24" + network = "my-network" + region = "${data.google_compute_regions.available.names[count.index]}" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `project` (Optional) - Project from which to list available regions. Defaults to project declared in the provider. +* `status` (Optional) - Allows to filter list of regions based on their current status. Status can be either `UP` or `DOWN`. + Defaults to no filtering (all available regions - both `UP` and `DOWN`). + +## Attributes Reference + +The following attribute is exported: + +* `names` - A list of regions available in the given project diff --git a/website/docs/index.html.markdown b/website/docs/index.html.markdown index 380bd17ffae..368f5e77ff8 100644 --- a/website/docs/index.html.markdown +++ b/website/docs/index.html.markdown @@ -35,8 +35,9 @@ resource "google_compute_instance" "default" { The following keys can be used to configure the provider. * `credentials` - (Optional) Contents of a file that contains your service - account private key in JSON format. You can download this file from the - Google Cloud Console. More details on retrieving this file are below. + account private key in JSON format. You can download your existing + [Google Cloud service account file] + from the Google Cloud Console, or you can create a new one from the same page. Credentials can also be specified using any of the following environment variables (listed in order of precedence): @@ -45,20 +46,21 @@ The following keys can be used to configure the provider. * `GOOGLE_CLOUD_KEYFILE_JSON` * `GCLOUD_KEYFILE_JSON` - The [`GOOGLE_APPLICATION_CREDENTIALS`](https://developers.google.com/identity/protocols/application-default-credentials#howtheywork) + The [`GOOGLE_APPLICATION_CREDENTIALS`][adc] environment variable can also contain the path of a file to obtain credentials from. If no credentials are specified, the provider will fall back to using the - [Google Application Default - Credentials](https://developers.google.com/identity/protocols/application-default-credentials). + [Google Application Default Credentials][adc]. If you are running Terraform from a GCE instance, see [Creating and Enabling - Service Accounts for - Instances](https://cloud.google.com/compute/docs/authentication) for - details. On your computer, if you have made your identity available as the + Service Accounts for Instances][gce-service-account] for details. + + On your computer, if you have made your identity available as the Application Default Credentials by running [`gcloud auth application-default - login`](https://cloud.google.com/sdk/gcloud/reference/auth/application-default/login), - the provider will use your identity. + login`][gcloud adc], the provider will use your identity. + + ~> **Warning:** The gcloud method is not guaranteed to work for all APIs, and + [service accounts] or [GCE metadata] should be used if possible. * `project` - (Optional) The ID of the project to apply any resources to. This can also be specified using any of the following environment variables (listed @@ -85,25 +87,6 @@ The following keys can be used to configure the provider. * `GCLOUD_ZONE` * `CLOUDSDK_COMPUTE_ZONE` -## Authentication JSON File - -Authenticating with Google Cloud services requires a JSON -file which we call the _account file_. - -This file is downloaded directly from the -[Google Developers Console](https://console.developers.google.com). To make -the process more straightforwarded, it is documented here: - -1. Log into the [Google Developers Console](https://console.developers.google.com) - and select a project. - -2. The API Manager view should be selected, click on "Credentials" on the left, - then "Create credentials", and finally "Service account key". - -3. Select "Compute Engine default service account" in the "Service account" - dropdown, and select "JSON" as the key type. - -4. Clicking "Create" will download your `credentials`. ## Beta Features @@ -115,3 +98,10 @@ is publicly announced, and is when they generally become publicly available. Terraform resources that support beta features will always use the Beta APIs to provision the resource. Importing a resource that supports beta features will always import those features, even if the resource was created in a matter that was not explicitly beta. + +[Google Cloud service account file]: https://console.cloud.google.com/apis/credentials/serviceaccountkey +[adc]: https://cloud.google.com/docs/authentication/production +[gce-service-account]: https://cloud.google.com/compute/docs/authentication +[gcloud adc]: https://cloud.google.com/sdk/gcloud/reference/auth/application-default/login +[service accounts]: https://cloud.google.com/docs/authentication/getting-started +[GCE metadata]: https://cloud.google.com/docs/authentication/production#obtaining_credentials_on_compute_engine_kubernetes_engine_app_engine_flexible_environment_and_cloud_functions diff --git a/website/docs/r/cloudbuild_trigger.html.markdown b/website/docs/r/cloudbuild_trigger.html.markdown index b080471bdf8..05d3ab4e19f 100644 --- a/website/docs/r/cloudbuild_trigger.html.markdown +++ b/website/docs/r/cloudbuild_trigger.html.markdown @@ -33,6 +33,21 @@ resource "google_cloudbuild_trigger" "build_trigger" { } ``` +OR + +```hcl +resource "google_cloudbuild_trigger" "build_trigger" { + project = "my-project" + trigger_template { + branch_name = "master" + project = "my-project" + repo_name = "some-repo" + } + filename = "cloudbuild.yaml" +} +``` + + ## Argument Reference (Argument descriptions sourced from https://godoc.org/google.golang.org/api/cloudbuild/v1#BuildTrigger) @@ -59,6 +74,10 @@ will be expanded when the build is created: or resolved from the specified branch or tag. * `$SHORT_SHA`: first 7 characters of `$REVISION_ID` or `$COMMIT_SHA`. +* `filename` - (Optional) Specify the path to a Cloud Build configuration file +in the Git repo. This is mutually exclusive with `build`. This is typically +`cloudbuild.yaml` however it can be specified by the user. + --- The `trigger_template` block supports: diff --git a/website/docs/r/compute_address.html.markdown b/website/docs/r/compute_address.html.markdown index 3ba1468716b..29b7d9ce078 100644 --- a/website/docs/r/compute_address.html.markdown +++ b/website/docs/r/compute_address.html.markdown @@ -51,6 +51,10 @@ The following arguments are supported: specified for INTERNAL address types. The IP address must be inside the specified subnetwork, if any. +* `network_tier` - (Optional) The [networking tier][network-tier] used for configuring + this address. This field can take the following values: PREMIUM or STANDARD. + If this field is not specified, it is assumed to be PREMIUM. + ## Attributes Reference In addition to the arguments listed above, the following computed attributes are @@ -86,3 +90,5 @@ $ terraform import google_compute_address.default https://www.googleapis.com/com $ terraform import google_compute_address.default projects/gcp-project/regions/us-central1/addresses/test-address ``` + +[network-tier]: https://cloud.google.com/network-tiers/docs/overview diff --git a/website/docs/r/compute_forwarding_rule.html.markdown b/website/docs/r/compute_forwarding_rule.html.markdown index ce5e9ca3205..f5b81e6d5c6 100644 --- a/website/docs/r/compute_forwarding_rule.html.markdown +++ b/website/docs/r/compute_forwarding_rule.html.markdown @@ -51,6 +51,10 @@ The following arguments are supported: should belong to. Only used for internal load balancing. If it is not provided, the default network is used. +* `network_tier` - (Optional) The [networking tier][network-tier] used for configuring + this forwarding rule. This field can take the following values: PREMIUM or STANDARD. + If this field is not specified, it is assumed to be PREMIUM. + * `port_range` - (Optional) A range e.g. "1024-2048" or a single port "1024" (defaults to all ports!). Only used for external load balancing. Some types of forwarding targets have constraints on the acceptable ports: @@ -91,3 +95,5 @@ Forwarding rules can be imported using the `name`, e.g. ``` $ terraform import google_compute_forwarding_rule.default website-forwarding-rule ``` + +[network-tier]: https://cloud.google.com/network-tiers/docs/overview diff --git a/website/docs/r/compute_global_forwarding_rule.html.markdown b/website/docs/r/compute_global_forwarding_rule.html.markdown index 6670847d922..eb96adbde51 100644 --- a/website/docs/r/compute_global_forwarding_rule.html.markdown +++ b/website/docs/r/compute_global_forwarding_rule.html.markdown @@ -101,6 +101,7 @@ The following arguments are supported: * `ip_version` - (Optional) The IP Version that will be used by this resource's address. One of `"IPV4"` or `"IPV6"`. + You cannot provide this and `ip_address`. - - - diff --git a/website/docs/r/compute_instance.html.markdown b/website/docs/r/compute_instance.html.markdown index aa6a0088fd6..b7a21f31f3b 100644 --- a/website/docs/r/compute_instance.html.markdown +++ b/website/docs/r/compute_instance.html.markdown @@ -227,6 +227,10 @@ The `access_config` block supports: See [the docs](https://cloud.google.com/compute/docs/instances/create-ptr-record) for how to become verified as a domain owner. +* `network_tier` - (Optional) The [networking tier][network-tier] used for configuring this instance. + This field can take the following values: PREMIUM or STANDARD. If this field is + not specified, it is assumed to be PREMIUM. + The `alias_ip_range` block supports: * `ip_cidr_range` - The IP CIDR range represented by this alias IP range. This IP CIDR range @@ -310,3 +314,4 @@ $ terraform import google_compute_instance.default gcp-project/us-central1-a/tes ``` [custom-vm-types]: https://cloud.google.com/dataproc/docs/concepts/compute/custom-machine-types +[network-tier]: https://cloud.google.com/network-tiers/docs/overview diff --git a/website/docs/r/compute_instance_group_manager.html.markdown b/website/docs/r/compute_instance_group_manager.html.markdown index 5f445e9e798..2fcd9a4f711 100644 --- a/website/docs/r/compute_instance_group_manager.html.markdown +++ b/website/docs/r/compute_instance_group_manager.html.markdown @@ -15,7 +15,7 @@ and [API](https://cloud.google.com/compute/docs/reference/latest/instanceGroupMa ~> **Note:** Use [google_compute_region_instance_group_manager](/docs/providers/google/r/compute_region_instance_group_manager.html) to create a regional (multi-zone) instance group manager. -## Example Usage +## Example Usage with top level instance template ```hcl resource "google_compute_health_check" "autohealing" { @@ -54,6 +54,30 @@ resource "google_compute_instance_group_manager" "appserver" { } ``` +## Example Usage with multiple Versions +```hcl +resource "google_compute_instance_group_manager" "appserver" { + name = "appserver-igm" + + base_instance_name = "app" + update_strategy = "NONE" + zone = "us-central1-a" + + target_size = 5 + + version { + instance_template = "${google_compute_instance_template.appserver.self_link}" + } + + version { + instance_template = "${google_compute_instance_template.appserver-canary.self_link}" + target_size { + fixed = 1 + } + } +} +``` + ## Argument Reference The following arguments are supported: @@ -65,8 +89,15 @@ The following arguments are supported: appending a hyphen and a random four-character string to the base instance name. -* `instance_template` - (Required) The full URL to an instance template from - which all new instances will be created. +* `instance_template` - (Optional) The full URL to an instance template from + which all new instances will be created. Conflicts with `version` (see [documentation](https://cloud.google.com/compute/docs/instance-groups/updating-managed-instance-groups#relationship_between_instancetemplate_properties_for_a_managed_instance_group)) + +* `version` - (Optional) Application versions managed by this instance group. Each + version deals with a specific instance template, allowing canary release scenarios. + Conflicts with `instance_template`. Structure is documented below. Beware that + exactly one version must not specify a target size. It means that versions with + a target size will respect the setting, and the one without target size will + be applied to all remaining Instances (top level target_size - each version target_size). * `name` - (Required) The name of the instance group manager. Must be 1-63 characters long and comply with @@ -154,6 +185,40 @@ The **auto_healing_policies** block supports: * `initial_delay_sec` - (Required) The number of seconds that the managed instance group waits before it applies autohealing policies to new instances or recently recreated instances. Between 0 and 3600. +The **version** block supports: + +```hcl +version { + name = "appserver-canary" + instance_template = "${google_compute_instance_template.appserver-canary.self_link}" + target_size { + fixed = 1 + } +} +``` + +```hcl +version { + name = "appserver-canary" + instance_template = "${google_compute_instance_template.appserver-canary.self_link}" + target_size { + percent = 20 + } +} +``` + +* `name` - (Required) - Version name. + +* `instance_template` - (Required) - The full URL to an instance template from which all new instances of this version will be created. + +* `target_size` - (Optional) - The number of instances calculated as a fixed number or a percentage depending on the settings. Structure is documented below. + +The **target_size** block supports: + +* `fixed` - (Optional), The number of instances which are managed for this version. Conflicts with `percent`. + +* `percent` - (Optional), The number of instances (calculated as percentage) which are managed for this version. Conflicts with `fixed`. + ## Attributes Reference In addition to the arguments listed above, the following computed attributes are diff --git a/website/docs/r/compute_instance_template.html.markdown b/website/docs/r/compute_instance_template.html.markdown index 2bc19d42bd8..ca2e760176e 100644 --- a/website/docs/r/compute_instance_template.html.markdown +++ b/website/docs/r/compute_instance_template.html.markdown @@ -249,6 +249,10 @@ The `access_config` block supports: * `nat_ip` - (Optional) The IP address that will be 1:1 mapped to the instance's network ip. If not given, one will be generated. +* `network_tier` - (Optional) The [networking tier][network-tier] used for configuring + this instance template. This field can take the following values: PREMIUM or + STANDARD. If this field is not specified, it is assumed to be PREMIUM. + The `alias_ip_range` block supports: * `ip_cidr_range` - The IP CIDR range represented by this alias IP range. This IP CIDR range @@ -312,3 +316,4 @@ $ terraform import google_compute_instance_template.default appserver-template ``` [custom-vm-types]: https://cloud.google.com/dataproc/docs/concepts/compute/custom-machine-types +[network-tier]: https://cloud.google.com/network-tiers/docs/overview diff --git a/website/docs/r/container_cluster.html.markdown b/website/docs/r/container_cluster.html.markdown index 21976de9849..72fdc59bc3e 100644 --- a/website/docs/r/container_cluster.html.markdown +++ b/website/docs/r/container_cluster.html.markdown @@ -109,8 +109,8 @@ output "cluster_ca_certificate" { Structure is documented below. * `logging_service` - (Optional) The logging service that the cluster should - write logs to. Available options include `logging.googleapis.com` and - `none`. Defaults to `logging.googleapis.com` + write logs to. Available options include `logging.googleapis.com`, + `logging.googleapis.com/kubernetes` (beta), and `none`. Defaults to `logging.googleapis.com` * `maintenance_policy` - (Optional) The maintenance policy to use for the cluster. Structure is documented below. @@ -137,8 +137,8 @@ output "cluster_ca_certificate" { Automatically send metrics from pods in the cluster to the Google Cloud Monitoring API. VM metrics will be collected by Google Compute Engine regardless of this setting Available options include - `monitoring.googleapis.com` and `none`. Defaults to - `monitoring.googleapis.com` + `monitoring.googleapis.com`, `monitoring.googleapis.com/kubernetes` (beta) and `none`. + Defaults to `monitoring.googleapis.com` * `network` - (Optional) The name or self_link of the Google Compute Engine network to which the cluster is connected. For Shared VPC, set this to the self link of the diff --git a/website/docs/r/redis_instance.html.markdown b/website/docs/r/redis_instance.html.markdown index cc2b98ae4bf..7284bc84124 100644 --- a/website/docs/r/redis_instance.html.markdown +++ b/website/docs/r/redis_instance.html.markdown @@ -155,6 +155,7 @@ This resource provides the following [Timeouts](/docs/configuration/resources.html#timeouts) configuration options: - `create` - Default is 6 minutes. +- `update` - Default is 4 minutes. - `delete` - Default is 4 minutes. ## Import diff --git a/website/google.erb b/website/google.erb index e85c7a14aec..b8cd3039d10 100644 --- a/website/google.erb +++ b/website/google.erb @@ -43,6 +43,9 @@ > google_project + > + google_compute_regions + > google_compute_ssl_policy @@ -91,6 +94,9 @@ > google_kms_secret + > + google_netblock_ip_ranges + > google_organization