diff --git a/google/provider.go b/google/provider.go index 0514436dc51..20379f73256 100644 --- a/google/provider.go +++ b/google/provider.go @@ -146,6 +146,7 @@ func Provider() terraform.ResourceProvider { "google_compute_route": resourceComputeRoute(), "google_compute_router": resourceComputeRouter(), "google_compute_router_interface": resourceComputeRouterInterface(), + "google_compute_router_nat": resourceComputeRouterNat(), "google_compute_router_peer": resourceComputeRouterPeer(), "google_compute_security_policy": resourceComputeSecurityPolicy(), "google_compute_shared_vpc_host_project": resourceComputeSharedVpcHostProject(), diff --git a/google/resource_compute_router_nat.go b/google/resource_compute_router_nat.go index 93cfad7a2a0..bc45b8de72d 100644 --- a/google/resource_compute_router_nat.go +++ b/google/resource_compute_router_nat.go @@ -1,3 +1,387 @@ package google -// Magic Modules doesn't let us remove files - blank out beta-only common-compile files for now. +import ( + "fmt" + "log" + "time" + + "strings" + + "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" +) + +var ( + routerNatSubnetworkConfig = &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "source_ip_ranges_to_nat": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "secondary_ip_range_names": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + } +) + +func resourceComputeRouterNat() *schema.Resource { + return &schema.Resource{ + // TODO(https://github.com/GoogleCloudPlatform/magic-modules/issues/963): Implement Update + Create: resourceComputeRouterNatCreate, + Read: resourceComputeRouterNatRead, + Delete: resourceComputeRouterNatDelete, + Importer: &schema.ResourceImporter{ + State: resourceComputeRouterNatImportState, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(10 * time.Minute), + Delete: schema.DefaultTimeout(10 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateRFC1035Name(2, 63), + }, + "router": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "nat_ip_allocate_option": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice([]string{"MANUAL_ONLY", "AUTO_ONLY"}, false), + }, + "nat_ips": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "source_subnetwork_ip_ranges_to_nat": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice([]string{"ALL_SUBNETWORKS_ALL_IP_RANGES", "ALL_SUBNETWORKS_ALL_PRIMARY_IP_RANGES", "LIST_OF_SUBNETWORKS"}, false), + }, + "subnetwork": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: routerNatSubnetworkConfig, + }, + "min_ports_per_vm": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + }, + "udp_idle_timeout_sec": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + }, + "icmp_idle_timeout_sec": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + }, + "tcp_established_idle_timeout_sec": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + }, + "tcp_transitory_idle_timeout_sec": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + }, + "project": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + "region": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + }, + } +} + +func resourceComputeRouterNatCreate(d *schema.ResourceData, meta interface{}) error { + + config := meta.(*Config) + + region, err := getRegion(d, config) + if err != nil { + return err + } + + project, err := getProject(d, config) + if err != nil { + return err + } + + routerName := d.Get("router").(string) + natName := d.Get("name").(string) + + routerLock := getRouterLockName(region, routerName) + mutexKV.Lock(routerLock) + defer mutexKV.Unlock(routerLock) + + routersService := config.clientComputeBeta.Routers + router, err := routersService.Get(project, region, routerName).Do() + if err != nil { + if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 { + return fmt.Errorf("Router %s/%s not found", region, routerName) + } + + return fmt.Errorf("Error Reading router %s/%s: %s", region, routerName, err) + } + + nats := router.Nats + for _, nat := range nats { + if nat.Name == natName { + return fmt.Errorf("Router %s has nat %s already", routerName, natName) + } + } + + nat := &computeBeta.RouterNat{ + Name: natName, + NatIpAllocateOption: d.Get("nat_ip_allocate_option").(string), + NatIps: convertStringArr(d.Get("nat_ips").(*schema.Set).List()), + SourceSubnetworkIpRangesToNat: d.Get("source_subnetwork_ip_ranges_to_nat").(string), + MinPortsPerVm: int64(d.Get("min_ports_per_vm").(int)), + UdpIdleTimeoutSec: int64(d.Get("udp_idle_timeout_sec").(int)), + IcmpIdleTimeoutSec: int64(d.Get("icmp_idle_timeout_sec").(int)), + TcpEstablishedIdleTimeoutSec: int64(d.Get("tcp_established_idle_timeout_sec").(int)), + TcpTransitoryIdleTimeoutSec: int64(d.Get("tcp_transitory_idle_timeout_sec").(int)), + } + + if v, ok := d.GetOk("subnetwork"); ok { + nat.Subnetworks = expandSubnetworks(v.(*schema.Set).List()) + } + + log.Printf("[INFO] Adding nat %s", natName) + nats = append(nats, nat) + patchRouter := &computeBeta.Router{ + Nats: nats, + } + + log.Printf("[DEBUG] Updating router %s/%s with nats: %+v", region, routerName, nats) + op, err := routersService.Patch(project, region, router.Name, patchRouter).Do() + if err != nil { + return fmt.Errorf("Error patching router %s/%s: %s", region, routerName, err) + } + d.SetId(fmt.Sprintf("%s/%s/%s", region, routerName, natName)) + err = computeBetaOperationWaitTime(config.clientCompute, op, project, "Patching router", int(d.Timeout(schema.TimeoutCreate).Minutes())) + if err != nil { + d.SetId("") + return fmt.Errorf("Error waiting to patch router %s/%s: %s", region, routerName, err) + } + + return resourceComputeRouterNatRead(d, meta) +} + +func resourceComputeRouterNatRead(d *schema.ResourceData, meta interface{}) error { + + config := meta.(*Config) + + region, err := getRegion(d, config) + if err != nil { + return err + } + + project, err := getProject(d, config) + if err != nil { + return err + } + + routerName := d.Get("router").(string) + natName := d.Get("name").(string) + + routersService := config.clientComputeBeta.Routers + router, err := routersService.Get(project, region, routerName).Do() + if err != nil { + if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 { + log.Printf("[WARN] Removing router nat %s because its router %s/%s is gone", natName, region, routerName) + d.SetId("") + + return nil + } + + return fmt.Errorf("Error Reading router %s/%s: %s", region, routerName, err) + } + + for _, nat := range router.Nats { + + if nat.Name == natName { + d.SetId(fmt.Sprintf("%s/%s/%s", region, routerName, natName)) + d.Set("nat_ip_allocate_option", nat.NatIpAllocateOption) + d.Set("nat_ips", schema.NewSet(schema.HashString, convertStringArrToInterface(convertSelfLinksToV1(nat.NatIps)))) + d.Set("source_subnetwork_ip_ranges_to_nat", nat.SourceSubnetworkIpRangesToNat) + d.Set("min_ports_per_vm", nat.MinPortsPerVm) + d.Set("udp_idle_timeout_sec", nat.UdpIdleTimeoutSec) + d.Set("icmp_idle_timeout_sec", nat.IcmpIdleTimeoutSec) + d.Set("tcp_established_idle_timeout_sec", nat.TcpEstablishedIdleTimeoutSec) + d.Set("tcp_transitory_idle_timeout_sec", nat.TcpTransitoryIdleTimeoutSec) + d.Set("region", region) + d.Set("project", project) + + if err := d.Set("subnetwork", flattenRouterNatSubnetworkToNatBeta(nat.Subnetworks)); err != nil { + return fmt.Errorf("Error reading router nat: %s", err) + } + + return nil + } + } + + log.Printf("[WARN] Removing router nat %s/%s/%s because it is gone", region, routerName, natName) + d.SetId("") + return nil +} + +func resourceComputeRouterNatDelete(d *schema.ResourceData, meta interface{}) error { + + config := meta.(*Config) + + region, err := getRegion(d, config) + if err != nil { + return err + } + + project, err := getProject(d, config) + if err != nil { + return err + } + + routerName := d.Get("router").(string) + natName := d.Get("name").(string) + + routerLock := getRouterLockName(region, routerName) + mutexKV.Lock(routerLock) + defer mutexKV.Unlock(routerLock) + + routersService := config.clientComputeBeta.Routers + router, err := routersService.Get(project, region, routerName).Do() + if err != nil { + if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 { + log.Printf("[WARN] Removing router nat %s because its router %s/%s is gone", natName, region, routerName) + + return nil + } + + return fmt.Errorf("Error Reading Router %s: %s", routerName, err) + } + + var newNats []*computeBeta.RouterNat = make([]*computeBeta.RouterNat, 0, len(router.Nats)) + for _, nat := range router.Nats { + if nat.Name == natName { + continue + } else { + newNats = append(newNats, nat) + } + } + + if len(newNats) == len(router.Nats) { + log.Printf("[DEBUG] Router %s/%s had no nat %s already", region, routerName, natName) + d.SetId("") + return nil + } + + log.Printf("[INFO] Removing nat %s from router %s/%s", natName, region, routerName) + patchRouter := &computeBeta.Router{ + Nats: newNats, + } + + if len(newNats) == 0 { + patchRouter.ForceSendFields = append(patchRouter.ForceSendFields, "Nats") + } + + log.Printf("[DEBUG] Updating router %s/%s with nats: %+v", region, routerName, newNats) + op, err := routersService.Patch(project, region, router.Name, patchRouter).Do() + if err != nil { + return fmt.Errorf("Error patching router %s/%s: %s", region, routerName, err) + } + + err = computeBetaOperationWaitTime(config.clientCompute, op, project, "Patching router", int(d.Timeout(schema.TimeoutDelete).Minutes())) + if err != nil { + return fmt.Errorf("Error waiting to patch router %s/%s: %s", region, routerName, err) + } + + d.SetId("") + return nil +} + +func resourceComputeRouterNatImportState(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + parts := strings.Split(d.Id(), "/") + if len(parts) != 3 { + return nil, fmt.Errorf("Invalid router nat specifier. Expecting {region}/{router}/{nat}") + } + + d.Set("region", parts[0]) + d.Set("router", parts[1]) + d.Set("name", parts[2]) + + return []*schema.ResourceData{d}, nil +} + +func expandSubnetworks(subnetworks []interface{}) []*computeBeta.RouterNatSubnetworkToNat { + result := make([]*computeBeta.RouterNatSubnetworkToNat, 0, len(subnetworks)) + + for _, subnetwork := range subnetworks { + snm := subnetwork.(map[string]interface{}) + subnetworkToNat := computeBeta.RouterNatSubnetworkToNat{ + Name: snm["name"].(string), + SourceIpRangesToNat: convertStringSet(snm["source_ip_ranges_to_nat"].(*schema.Set)), + } + if v, ok := snm["secondary_ip_range_names"]; ok { + subnetworkToNat.SecondaryIpRangeNames = convertStringSet(v.(*schema.Set)) + } + result = append(result, &subnetworkToNat) + } + + return result +} + +func flattenRouterNatSubnetworkToNatBeta(subnetworksToNat []*computeBeta.RouterNatSubnetworkToNat) []map[string]interface{} { + result := make([]map[string]interface{}, 0, len(subnetworksToNat)) + for _, subnetworkToNat := range subnetworksToNat { + stnMap := make(map[string]interface{}) + stnMap["name"] = ConvertSelfLinkToV1(subnetworkToNat.Name) + stnMap["source_ip_ranges_to_nat"] = schema.NewSet(schema.HashString, convertStringArrToInterface(subnetworkToNat.SourceIpRangesToNat)) + stnMap["secondary_ip_range_names"] = schema.NewSet(schema.HashString, convertStringArrToInterface(subnetworkToNat.SecondaryIpRangeNames)) + result = append(result, stnMap) + } + return result +} + +func convertSelfLinksToV1(selfLinks []string) []string { + result := make([]string, 0, len(selfLinks)) + for _, selfLink := range selfLinks { + result = append(result, ConvertSelfLinkToV1(selfLink)) + } + return result +} diff --git a/google/resource_compute_router_nat_test.go b/google/resource_compute_router_nat_test.go index 93cfad7a2a0..3a728cb9cc2 100644 --- a/google/resource_compute_router_nat_test.go +++ b/google/resource_compute_router_nat_test.go @@ -1,3 +1,224 @@ package google -// Magic Modules doesn't let us remove files - blank out beta-only common-compile files for now. +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccComputeRouterNat_basic(t *testing.T) { + t.Parallel() + + testId := acctest.RandString(10) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeRouterNatDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeRouterNatBasic(testId), + }, + resource.TestStep{ + ResourceName: "google_compute_router_nat.foobar", + ImportState: true, + ImportStateVerify: true, + }, + resource.TestStep{ + Config: testAccComputeRouterNatKeepRouter(testId), + Check: testAccCheckComputeRouterNatDelete( + "google_compute_router_nat.foobar"), + }, + }, + }) +} + +func TestAccComputeRouterNat_withManualIpAndSubnetConfiguration(t *testing.T) { + t.Parallel() + + testId := acctest.RandString(10) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeRouterNatDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeRouterNatWithManualIpAndSubnetConfiguration(testId), + }, + resource.TestStep{ + ResourceName: "google_compute_router_nat.foobar", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckComputeRouterNatDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + + routersService := config.clientCompute.Routers + + for _, rs := range s.RootModule().Resources { + if rs.Type != "google_compute_router" { + continue + } + + project, err := getTestProject(rs.Primary, config) + if err != nil { + return err + } + + region, err := getTestRegion(rs.Primary, config) + if err != nil { + return err + } + + routerName := rs.Primary.Attributes["router"] + + _, err = routersService.Get(project, region, routerName).Do() + + if err == nil { + return fmt.Errorf("Error, Router %s in region %s still exists", + routerName, region) + } + } + + return nil +} + +func testAccCheckComputeRouterNatDelete(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + + routersService := config.clientComputeBeta.Routers + + for _, rs := range s.RootModule().Resources { + if rs.Type != "google_compute_router_nat" { + continue + } + + project, err := getTestProject(rs.Primary, config) + if err != nil { + return err + } + + region, err := getTestRegion(rs.Primary, config) + if err != nil { + return err + } + + name := rs.Primary.Attributes["name"] + routerName := rs.Primary.Attributes["router"] + + router, err := routersService.Get(project, region, routerName).Do() + + if err != nil { + return fmt.Errorf("Error Reading Router %s: %s", routerName, err) + } + + nats := router.Nats + for _, nat := range nats { + + if nat.Name == name { + return fmt.Errorf("Nat %s still exists on router %s/%s", name, region, router.Name) + } + } + } + + return nil + } +} + +func testAccComputeRouterNatBasic(testId string) string { + return fmt.Sprintf(` + resource "google_compute_network" "foobar" { + name = "router-nat-test-%s" + } + resource "google_compute_subnetwork" "foobar" { + name = "router-nat-test-subnetwork-%s" + network = "${google_compute_network.foobar.self_link}" + ip_cidr_range = "10.0.0.0/16" + region = "us-central1" + } + resource "google_compute_router" "foobar"{ + name = "router-nat-test-%s" + region = "${google_compute_subnetwork.foobar.region}" + network = "${google_compute_network.foobar.self_link}" + bgp { + asn = 64514 + } + } + resource "google_compute_router_nat" "foobar" { + name = "router-nat-test-%s" + router = "${google_compute_router.foobar.name}" + region = "${google_compute_router.foobar.region}" + nat_ip_allocate_option = "AUTO_ONLY" + source_subnetwork_ip_ranges_to_nat = "ALL_SUBNETWORKS_ALL_IP_RANGES" + } + `, testId, testId, testId, testId) +} + +func testAccComputeRouterNatWithManualIpAndSubnetConfiguration(testId string) string { + return fmt.Sprintf(` + resource "google_compute_network" "foobar" { + name = "router-nat-test-%s" + auto_create_subnetworks = "false" + } + resource "google_compute_subnetwork" "foobar" { + name = "router-nat-test-subnetwork-%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 = "router-nat-test-%s" + region = "${google_compute_subnetwork.foobar.region}" + } + resource "google_compute_router" "foobar"{ + name = "router-nat-test-%s" + region = "${google_compute_subnetwork.foobar.region}" + network = "${google_compute_network.foobar.self_link}" + bgp { + asn = 64514 + } + } + resource "google_compute_router_nat" "foobar" { + name = "router-nat-test-%s" + router = "${google_compute_router.foobar.name}" + region = "${google_compute_router.foobar.region}" + nat_ip_allocate_option = "MANUAL_ONLY" + nat_ips = ["${google_compute_address.foobar.self_link}"] + source_subnetwork_ip_ranges_to_nat = "LIST_OF_SUBNETWORKS" + subnetwork { + name = "${google_compute_subnetwork.foobar.self_link}" + source_ip_ranges_to_nat = ["ALL_IP_RANGES"] + } + } + `, testId, testId, testId, testId, testId) +} + +func testAccComputeRouterNatKeepRouter(testId string) string { + return fmt.Sprintf(` + resource "google_compute_network" "foobar" { + name = "router-nat-test-%s" + auto_create_subnetworks = "false" + } + resource "google_compute_subnetwork" "foobar" { + name = "router-nat-test-subnetwork-%s" + network = "${google_compute_network.foobar.self_link}" + ip_cidr_range = "10.0.0.0/16" + region = "us-central1" + } + resource "google_compute_router" "foobar"{ + name = "router-nat-test-%s" + region = "${google_compute_subnetwork.foobar.region}" + network = "${google_compute_network.foobar.self_link}" + bgp { + asn = 64514 + } + } + `, testId, testId, testId) +}