From 043e816969f040162e85d34662a0e784c21355e2 Mon Sep 17 00:00:00 2001 From: Vincent Roseberry Date: Fri, 4 Aug 2017 11:00:45 -0700 Subject: [PATCH] google_compute_instance can specified the subnetwork using a self_link (#290) --- google/provider.go | 41 ++++++++++++++ google/resource_compute_instance.go | 53 ++++++++++--------- google/resource_compute_instance_test.go | 2 +- google/validation.go | 10 +++- website/docs/r/compute_instance.html.markdown | 8 +-- 5 files changed, 83 insertions(+), 31 deletions(-) diff --git a/google/provider.go b/google/provider.go index ae2c9a2ef3d..769c8d7f6cd 100644 --- a/google/provider.go +++ b/google/provider.go @@ -13,6 +13,7 @@ import ( computeBeta "google.golang.org/api/compute/v0.beta" "google.golang.org/api/compute/v1" "google.golang.org/api/googleapi" + "regexp" ) // Global MutexKV @@ -273,6 +274,46 @@ func getNetworkLink(d *schema.ResourceData, config *Config, field string) (strin } } +// Reads the "subnetwork" fields from the given resource data and if the value is: +// - a resource URL, returns the string unchanged +// - a subnetwork name, looks up the resource URL using the google client. +// +// If `subnetworkField` is a resource url, `subnetworkProjectField` cannot be set. +// If `subnetworkField` is a subnetwork name, `subnetworkProjectField` will be used +// as the project if set. If not, we fallback on the default project. +func getSubnetworkLink(d *schema.ResourceData, config *Config, subnetworkField, subnetworkProjectField, zoneField string) (string, error) { + if v, ok := d.GetOk(subnetworkField); ok { + subnetwork := v.(string) + r := regexp.MustCompile(SubnetworkLinkRegex) + if r.MatchString(subnetwork) { + return subnetwork, nil + } + + var project string + if subnetworkProject, ok := d.GetOk(subnetworkProjectField); ok { + project = subnetworkProject.(string) + } else { + var err error + project, err = getProject(d, config) + if err != nil { + return "", err + } + } + + region := getRegionFromZone(d.Get(zoneField).(string)) + + subnet, err := config.clientCompute.Subnetworks.Get(project, region, subnetwork).Do() + if err != nil { + return "", fmt.Errorf( + "Error referencing subnetwork '%s' in region '%s': %s", + subnetwork, region, err) + } + + return subnet.SelfLink, nil + } + return "", nil +} + // getNetworkName reads the "network" field from the given resource data and if the value: // - is a resource URL, extracts the network name from the URL and returns it // - is the network name only (i.e not prefixed with http://www.googleapis.com/compute/...), is returned unchanged diff --git a/google/resource_compute_instance.go b/google/resource_compute_instance.go index e8cf14097fd..12460f29c74 100644 --- a/google/resource_compute_instance.go +++ b/google/resource_compute_instance.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/terraform/helper/validation" "google.golang.org/api/compute/v1" "google.golang.org/api/googleapi" + "regexp" ) func stringScopeHashcode(v interface{}) int { @@ -278,20 +279,26 @@ func resourceComputeInstance() *schema.Resource { Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "network": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - ForceNew: true, + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + DiffSuppressFunc: linkDiffSuppress, + ConflictsWith: []string{"network_interface.0.subnetwork", "network_interface.0.subnetwork_project"}, }, "subnetwork": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - ForceNew: true, + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + DiffSuppressFunc: linkDiffSuppress, }, "subnetwork_project": &schema.Schema{ Type: schema.TypeString, Optional: true, + Computed: true, ForceNew: true, }, @@ -704,34 +711,21 @@ func resourceComputeInstanceCreate(d *schema.ResourceData, meta interface{}) err prefix := fmt.Sprintf("network_interface.%d", i) // Load up the name of this network_interface networkName := d.Get(prefix + ".network").(string) - subnetworkName := d.Get(prefix + ".subnetwork").(string) - subnetworkProject := d.Get(prefix + ".subnetwork_project").(string) address := d.Get(prefix + ".address").(string) var networkLink, subnetworkLink string - if networkName != "" && subnetworkName != "" { - return fmt.Errorf("Cannot specify both network and subnetwork values.") - } else if networkName != "" { + if networkName != "" { networkLink, err = getNetworkLink(d, config, prefix+".network") if err != nil { return fmt.Errorf( "Error referencing network '%s': %s", networkName, err) } - } else { - region := getRegionFromZone(d.Get("zone").(string)) - if subnetworkProject == "" { - subnetworkProject = project - } - subnetwork, err := config.clientCompute.Subnetworks.Get( - subnetworkProject, region, subnetworkName).Do() + subnetworkLink, err = getSubnetworkLink(d, config, prefix+".subnetwork", prefix+".subnetwork_project", "zone") if err != nil { - return fmt.Errorf( - "Error referencing subnetwork '%s' in region '%s': %s", - subnetworkName, region, err) + return err } - subnetworkLink = subnetwork.SelfLink } // Build the networkInterface @@ -961,9 +955,9 @@ func resourceComputeInstanceRead(d *schema.ResourceData, meta interface{}) error networkInterfaces = append(networkInterfaces, map[string]interface{}{ "name": iface.Name, "address": iface.NetworkIP, - "network": d.Get(fmt.Sprintf("network_interface.%d.network", i)), - "subnetwork": d.Get(fmt.Sprintf("network_interface.%d.subnetwork", i)), - "subnetwork_project": d.Get(fmt.Sprintf("network_interface.%d.subnetwork_project", i)), + "network": iface.Network, + "subnetwork": iface.Subnetwork, + "subnetwork_project": getProjectFromSubnetworkLink(iface.Subnetwork), "access_config": accessConfigs, }) } @@ -1452,3 +1446,12 @@ func flattenScratchDisk(disk *compute.AttachedDisk) map[string]interface{} { } return result } + +func getProjectFromSubnetworkLink(subnetwork string) string { + r := regexp.MustCompile(SubnetworkLinkRegex) + if !r.MatchString(subnetwork) { + return "" + } + + return r.FindStringSubmatch(subnetwork)[1] +} diff --git a/google/resource_compute_instance_test.go b/google/resource_compute_instance_test.go index 23841d1f131..9b1b86c8182 100644 --- a/google/resource_compute_instance_test.go +++ b/google/resource_compute_instance_test.go @@ -1609,7 +1609,7 @@ resource "google_compute_instance" "foobar" { } network_interface { - subnetwork = "${google_compute_subnetwork.inst-test-subnetwork.name}" + subnetwork = "${google_compute_subnetwork.inst-test-subnetwork.self_link}" access_config { } } diff --git a/google/validation.go b/google/validation.go index 2ffa3e26c6d..1feebf1a322 100644 --- a/google/validation.go +++ b/google/validation.go @@ -6,8 +6,14 @@ 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])?))" +const ( + // Copied from the official Google Cloud auto-generated client. + 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])?))" + RegionRegex = "[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?" + SubnetworkRegex = "[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?" + + SubnetworkLinkRegex = "projects/(" + ProjectRegex + ")/regions/(" + RegionRegex + ")/subnetworks/(" + SubnetworkRegex + ")$" +) func validateGCPName(v interface{}, k string) (ws []string, errors []error) { re := `^(?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?)$` diff --git a/website/docs/r/compute_instance.html.markdown b/website/docs/r/compute_instance.html.markdown index 2531fb53e84..9b1f9b9a1e3 100644 --- a/website/docs/r/compute_instance.html.markdown +++ b/website/docs/r/compute_instance.html.markdown @@ -210,12 +210,14 @@ The `network_interface` block supports: * `network` - (Optional) The name or self_link of the network to attach this interface to. Either `network` or `subnetwork` must be provided. -* `subnetwork` - (Optional) The name of the subnetwork to attach this interface - to. The subnetwork must exist in the same region this instance will be +* `subnetwork` - (Optional) The name or self_link of the subnetwork to attach this + interface to. The subnetwork must exist in the same region this instance will be created in. Either `network` or `subnetwork` must be provided. * `subnetwork_project` - (Optional) The project in which the subnetwork belongs. - If it is not provided, the provider project is used. + If the `subnetwork` is a self_link, this field is ignored in favor of the project + defined in the subnetwork self_link. If the `subnetwork` is a name and this + field is not provided, the provider project is used. * `address` - (Optional) The private IP address to assign to the instance. If empty, the address will be automatically assigned.