diff --git a/google/provider.go b/google/provider.go index f24dd5e01f2..bfec54d95ef 100644 --- a/google/provider.go +++ b/google/provider.go @@ -86,6 +86,7 @@ func Provider() terraform.ResourceProvider { "google_compute_instance_group_manager": resourceComputeInstanceGroupManager(), "google_compute_instance_template": resourceComputeInstanceTemplate(), "google_compute_network": resourceComputeNetwork(), + "google_compute_network_peering": resourceComputeNetworkPeering(), "google_compute_project_metadata": resourceComputeProjectMetadata(), "google_compute_region_backend_service": resourceComputeRegionBackendService(), "google_compute_route": resourceComputeRoute(), diff --git a/google/resource_compute_network_peering.go b/google/resource_compute_network_peering.go new file mode 100644 index 00000000000..06f1e4b1cfe --- /dev/null +++ b/google/resource_compute_network_peering.go @@ -0,0 +1,189 @@ +package google + +import ( + "fmt" + "log" + "regexp" + "sort" + + "github.com/hashicorp/terraform/helper/schema" + "google.golang.org/api/compute/v1" + "google.golang.org/api/googleapi" +) + +const peerNetworkLinkRegex = "projects/(" + ProjectRegex + ")/global/networks/((?:[a-z](?:[-a-z0-9]*[a-z0-9])?))$" + +func resourceComputeNetworkPeering() *schema.Resource { + return &schema.Resource{ + Create: resourceComputeNetworkPeeringCreate, + Read: resourceComputeNetworkPeeringRead, + Delete: resourceComputeNetworkPeeringDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateGCPName, + }, + "network": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateRegexp(peerNetworkLinkRegex), + DiffSuppressFunc: compareSelfLinkRelativePaths, + }, + "peer_network": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateRegexp(peerNetworkLinkRegex), + DiffSuppressFunc: compareSelfLinkRelativePaths, + }, + "auto_create_routes": &schema.Schema{ + Type: schema.TypeBool, + ForceNew: true, + Optional: true, + Default: true, + }, + "state": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "state_details": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceComputeNetworkPeeringCreate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + project, err := getProject(d, config) + if err != nil { + return err + } + + networkLink := d.Get("network").(string) + networkName := getNameFromNetworkLink(networkLink) + + request := &compute.NetworksAddPeeringRequest{ + Name: d.Get("name").(string), + PeerNetwork: d.Get("peer_network").(string), + AutoCreateRoutes: d.Get("auto_create_routes").(bool), + } + + addOp, err := config.clientCompute.Networks.AddPeering(project, networkName, request).Do() + if err != nil { + return fmt.Errorf("Error adding network peering: %s", err) + } + + err = computeOperationWait(config, addOp, project, "Adding Network Peering") + if err != nil { + return err + } + + d.SetId(fmt.Sprintf("%s/%s", networkName, d.Get("name").(string))) + + return resourceComputeNetworkPeeringRead(d, meta) +} + +func resourceComputeNetworkPeeringRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + project, err := getProject(d, config) + if err != nil { + return err + } + + peeringName := d.Get("name").(string) + networkLink := d.Get("network").(string) + networkName := getNameFromNetworkLink(networkLink) + + network, err := config.clientCompute.Networks.Get(project, networkName).Do() + if err != nil { + return handleNotFoundError(err, d, fmt.Sprintf("Network %q", networkName)) + } + + peering := findPeeringFromNetwork(network, peeringName) + if peering == nil { + log.Printf("[WARN] Removing network peering %s from network %s because it's gone", peeringName, networkName) + d.SetId("") + return nil + } + + d.Set("peer_network", peering.Network) + d.Set("auto_create_routes", peering.AutoCreateRoutes) + d.Set("state", peering.State) + d.Set("state_details", peering.StateDetails) + + return nil +} + +func resourceComputeNetworkPeeringDelete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + // Remove the `network` to `peer_network` peering + project, err := getProject(d, config) + if err != nil { + return err + } + + name := d.Get("name").(string) + networkLink := d.Get("network").(string) + peerNetworkLink := d.Get("peer_network").(string) + networkName := getNameFromNetworkLink(networkLink) + peerNetworkName := getNameFromNetworkLink(peerNetworkLink) + + request := &compute.NetworksRemovePeeringRequest{ + Name: name, + } + + // Only one delete peering operation at a time can be performed inside any peered VPCs. + peeringLockName := getNetworkPeeringLockName(networkName, peerNetworkName) + mutexKV.Lock(peeringLockName) + defer mutexKV.Unlock(peeringLockName) + + removeOp, err := config.clientCompute.Networks.RemovePeering(project, networkName, request).Do() + if err != nil { + if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 { + log.Printf("[WARN] Peering `%s` already removed from network `%s`", name, networkName) + } else { + return fmt.Errorf("Error removing peering `%s` from network `%s`: %s", name, networkName, err) + } + } else { + err = computeOperationWait(config, removeOp, project, "Removing Network Peering") + if err != nil { + return err + } + } + + return nil +} + +func findPeeringFromNetwork(network *compute.Network, peeringName string) *compute.NetworkPeering { + for _, p := range network.Peerings { + if p.Name == peeringName { + return p + } + } + return nil +} + +func getNameFromNetworkLink(network string) string { + r := regexp.MustCompile(peerNetworkLinkRegex) + + m := r.FindStringSubmatch(network) + return m[2] +} + +func getNetworkPeeringLockName(networkName, peerNetworkName string) string { + // Whether you delete the peering from network A to B or the one from B to A, they + // cannot happen at the same time. + networks := []string{networkName, peerNetworkName} + sort.Strings(networks) + + return fmt.Sprintf("network_peering/%s/%s", networks[0], networks[1]) +} diff --git a/google/resource_compute_network_peering_test.go b/google/resource_compute_network_peering_test.go new file mode 100644 index 00000000000..8852eca16cd --- /dev/null +++ b/google/resource_compute_network_peering_test.go @@ -0,0 +1,121 @@ +package google + +import ( + "fmt" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "google.golang.org/api/compute/v1" + "strings" + "testing" +) + +func TestAccComputeNetworkPeering_basic(t *testing.T) { + var peering compute.NetworkPeering + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccComputeNetworkPeeringDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeNetworkPeering_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeNetworkPeeringExist("google_compute_network_peering.foo", &peering), + testAccCheckComputeNetworkPeeringAutoCreateRoutes(true, &peering), + testAccCheckComputeNetworkPeeringExist("google_compute_network_peering.bar", &peering), + testAccCheckComputeNetworkPeeringAutoCreateRoutes(true, &peering), + ), + }, + }, + }) + +} + +func testAccComputeNetworkPeeringDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "google_compute_network_peering" { + continue + } + + _, err := config.clientCompute.Networks.Get( + config.Project, rs.Primary.ID).Do() + if err == nil { + return fmt.Errorf("Network peering still exists") + } + } + + return nil +} + +func testAccCheckComputeNetworkPeeringExist(n string, peering *compute.NetworkPeering) 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) + + parts := strings.Split(rs.Primary.ID, "/") + if len(parts) != 2 { + return fmt.Errorf("Invalid network peering identifier: %s", rs.Primary.ID) + } + + networkName, peeringName := parts[0], parts[1] + + network, err := config.clientCompute.Networks.Get(config.Project, networkName).Do() + if err != nil { + return err + } + + found := findPeeringFromNetwork(network, peeringName) + if found == nil { + return fmt.Errorf("Network peering '%s' not found in network '%s'", peeringName, network.Name) + } + *peering = *found + + return nil + } +} + +func testAccCheckComputeNetworkPeeringAutoCreateRoutes(v bool, peering *compute.NetworkPeering) resource.TestCheckFunc { + return func(s *terraform.State) error { + if peering.AutoCreateRoutes != v { + return fmt.Errorf("should AutoCreateRoutes set to %t", v) + } + + return nil + } +} + +var testAccComputeNetworkPeering_basic = fmt.Sprintf(` +resource "google_compute_network" "network1" { + name = "network-test-1-%s" + auto_create_subnetworks = false +} + +resource "google_compute_network" "network2" { + name = "network-test-2-%s" + auto_create_subnetworks = false +} + +resource "google_compute_network_peering" "foo" { + name = "peering-test-1-%s" + network = "${google_compute_network.network1.self_link}" + peer_network = "${google_compute_network.network2.self_link}" +} + +resource "google_compute_network_peering" "bar" { + name = "peering-test-2-%s" + auto_create_routes = true + network = "${google_compute_network.network2.self_link}" + peer_network = "${google_compute_network.network1.self_link}" +} +`, acctest.RandString(10), acctest.RandString(10), acctest.RandString(10), acctest.RandString(10)) diff --git a/google/validation.go b/google/validation.go index 4191b6e8692..2ffa3e26c6d 100644 --- a/google/validation.go +++ b/google/validation.go @@ -6,6 +6,9 @@ import ( "regexp" ) +// Copied from the official Google Cloud auto-generated client. +const ProjectRegex = "(?:(?:[-a-z0-9]{1,63}\\.)*(?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?):)?(?:[0-9]{1,19}|(?:[a-z0-9](?:[-a-z0-9]{0,61}[a-z0-9])?))" + func validateGCPName(v interface{}, k string) (ws []string, errors []error) { re := `^(?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?)$` return validateRegexp(re)(v, k) diff --git a/website/docs/r/compute_network_peering.html.markdown b/website/docs/r/compute_network_peering.html.markdown new file mode 100644 index 00000000000..e649c0ee2ac --- /dev/null +++ b/website/docs/r/compute_network_peering.html.markdown @@ -0,0 +1,66 @@ +--- +layout: "google" +page_title: "Google: google_compute_network_peering" +sidebar_current: "docs-google-compute-network-peering" +description: |- + Manages a network peering within GCE. +--- + +# google\_compute\_network\_peering + +Manages a network peering within GCE. For more information see +[the official documentation](https://cloud.google.com/compute/docs/vpc/vpc-peering) +and +[API](https://cloud.google.com/compute/docs/reference/latest/networks). + +~> **Note:** Both network must create a peering with each other for the peering to be functional. + +~> **Note:** Subnets IP ranges across peered VPC networks cannot overlap. + +## Example Usage + +```hcl +resource "google_compute_network_peering" "peering1" { + name = "peering1" + network = "${google_compute_network.default.self_link}" + peer_network = "${google_compute_network.other.self_link}" +} + +resource "google_compute_network_peering" "peering2" { + name = "peering2" + network = "${google_compute_network.other.self_link}" + peer_network = "${google_compute_network.default.self_link}" +} + +resource "google_compute_network" "default" { + name = "foobar" + auto_create_subnetworks = "false" +} + +resource "google_compute_network" "other" { + name = "other" + auto_create_subnetworks = "false" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) Name of the peering. + +* `network` - (Required) Resource link of the network to add a peering to. + +* `peer_network` - (Required) Resource link of the peer network. + +* `auto_create_routes` - (Optional) If set to `true`, the routes between the two networks will + be created and managed automatically. Defaults to `true`. + +## Attributes Reference + +In addition to the arguments listed above, the following computed attributes are +exported: + +* `state` - State for the peering. + +* `state_details` - Details about the current state of the peering.