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..6d4674f3c77 --- /dev/null +++ b/google/resource_compute_network_peering.go @@ -0,0 +1,229 @@ +package google + +import ( + "fmt" + "github.com/hashicorp/terraform/helper/schema" + "google.golang.org/api/compute/v1" + "google.golang.org/api/googleapi" + "log" + "regexp" +) + +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: peerNetworkLinkDiffSuppress, + }, + "peer_network": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateRegexp(peerNetworkLinkRegex), + DiffSuppressFunc: peerNetworkLinkDiffSuppress, + }, + "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) + + err := addPeering(config, d) + if err != nil { + return err + } + + peeringName := d.Get("name").(string) + networkName := getNameFromNetworkLink(d.Get("network").(string)) + + d.SetId(fmt.Sprintf("%s/%s", networkName, peeringName)) + + 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 + } + + // No need to set the `name` and `network` fields. We use both of them to find the peering. + // If they change on GCP, we wouldn't have been able to find the peering in the first place. + 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 + err := removePeering(config, d) + 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 addPeering(config *Config, d *schema.ResourceData) error { + 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) + autoCreateRoutes := d.Get("auto_create_routes").(bool) + networkName := getNameFromNetworkLink(networkLink) + + request := &compute.NetworksAddPeeringRequest{ + Name: name, + PeerNetwork: peerNetworkLink, + AutoCreateRoutes: autoCreateRoutes, + } + + 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 + } + + return nil +} + +func removePeering(config *Config, d *schema.ResourceData) error { + 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 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 getNameFromNetworkLink(network string) string { + r := regexp.MustCompile(peerNetworkLinkRegex) + + m := r.FindStringSubmatch(network) + return m[2] +} + +func peerNetworkLinkDiffSuppress(k, old, new string, d *schema.ResourceData) bool { + r := regexp.MustCompile(peerNetworkLinkRegex) + + m := r.FindStringSubmatch(old) + if len(m) != 3 { + return false + } + oldProject, oldPeeringNetworkName := m[1], m[2] + + m = r.FindStringSubmatch(new) + if len(m) != 3 { + return false + } + newProject, newPeeringNetworkName := m[1], m[2] + + if oldProject == newProject && oldPeeringNetworkName == newPeeringNetworkName { + return true + } + return false +} + +func getNetworkPeeringLockName(networkName, peerNetworkName string) string { + return fmt.Sprintf("network_peering/%s/%s") +} diff --git a/google/resource_compute_network_peering_test.go b/google/resource_compute_network_peering_test.go new file mode 100644 index 00000000000..851309d1dac --- /dev/null +++ b/google/resource_compute_network_peering_test.go @@ -0,0 +1,123 @@ +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..4d786bb0b23 100644 --- a/google/validation.go +++ b/google/validation.go @@ -6,6 +6,8 @@ import ( "regexp" ) +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..0e675e71ef8 --- /dev/null +++ b/website/docs/r/compute_network_peering.html.markdown @@ -0,0 +1,76 @@ +--- +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. + +## Example Usage + +```hcl +resource "google_compute_network" "default" { + name = "foobar" + auto_create_subnetworks = "false" +} + +resource "google_compute_network" "other" { + name = "other" + auto_create_subnetworks = "false" +} + +// Both network must create a peering with each other for the peering +// to be functional. +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}" +} + +// Subnets IP ranges across peered VPC networks cannot overlap. +resource "google_compute_subnetwork" "network1-subnet1" { + name = "network1-sub1" + ip_cidr_range = "10.128.0.0/20" + network = "${google_compute_network.network1.self_link}" + region = "us-east1" +} + +resource "google_compute_subnetwork" "network2-subnet1" { + name = "network1-sub2" + ip_cidr_range = "10.132.0.0/20" + network = "${google_compute_network.network2.self_link}" + region = "us-central1" +} +``` + +## 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` - (Computed) State for the peering. + +* `state_details` - (Computed) Details about the current state of the peering.