Skip to content

Commit

Permalink
Merge pull request hashicorp#84 from craigatgoogle/vpc_peering_connec…
Browse files Browse the repository at this point in the history
…tion

Implemented support for VPC Connection Peering
  • Loading branch information
danawillow authored and nat-henderson committed Dec 12, 2018
1 parent cd1bff4 commit 7417340
Show file tree
Hide file tree
Showing 10 changed files with 7,169 additions and 0 deletions.
9 changes: 9 additions & 0 deletions google-beta/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import (
"google.golang.org/api/redis/v1beta1"
"google.golang.org/api/runtimeconfig/v1beta1"
"google.golang.org/api/servicemanagement/v1"
"google.golang.org/api/servicenetworking/v1beta"
"google.golang.org/api/serviceusage/v1beta1"
"google.golang.org/api/sourcerepo/v1"
"google.golang.org/api/spanner/v1"
Expand Down Expand Up @@ -91,6 +92,7 @@ type Config struct {
clientCloudFunctions *cloudfunctions.Service
clientCloudIoT *cloudiot.Service
clientAppEngine *appengine.APIService
clientServiceNetworking *servicenetworking.APIService

bigtableClientFactory *BigtableClientFactory
}
Expand Down Expand Up @@ -379,6 +381,13 @@ func (c *Config) loadAndValidate() error {
}
c.clientComposer.UserAgent = userAgent

log.Printf("[INFO] Instantiating Service Networking Client...")
c.clientServiceNetworking, err = servicenetworking.New(client)
if err != nil {
return err
}
c.clientServiceNetworking.UserAgent = userAgent

return nil
}

Expand Down
1 change: 1 addition & 0 deletions google-beta/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ func Provider() terraform.ResourceProvider {
"google_spanner_database_iam_binding": ResourceIamBindingWithImport(IamSpannerDatabaseSchema, NewSpannerDatabaseIamUpdater, SpannerDatabaseIdParseFunc),
"google_spanner_database_iam_member": ResourceIamMemberWithImport(IamSpannerDatabaseSchema, NewSpannerDatabaseIamUpdater, SpannerDatabaseIdParseFunc),
"google_spanner_database_iam_policy": ResourceIamPolicyWithImport(IamSpannerDatabaseSchema, NewSpannerDatabaseIamUpdater, SpannerDatabaseIdParseFunc),
"google_service_networking_connection": resourceServiceNetworkingConnection(),
"google_sql_database": resourceSqlDatabase(),
"google_sql_database_instance": resourceSqlDatabaseInstance(),
"google_sql_ssl_cert": resourceSqlSslCert(),
Expand Down
235 changes: 235 additions & 0 deletions google-beta/resource_service_networking_connection.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
package google

import (
"fmt"
"log"
"net/url"
"regexp"
"strings"

"github.com/hashicorp/terraform/helper/schema"
"google.golang.org/api/servicenetworking/v1beta"
)

func resourceServiceNetworkingConnection() *schema.Resource {
return &schema.Resource{
Create: resourceServiceNetworkingConnectionCreate,
Read: resourceServiceNetworkingConnectionRead,
Update: resourceServiceNetworkingConnectionUpdate,
Delete: resourceServiceNetworkingConnectionDelete,
Importer: &schema.ResourceImporter{
State: resourceServiceNetworkingConnectionImportState,
},

Schema: map[string]*schema.Schema{
"network": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
DiffSuppressFunc: compareSelfLinkOrResourceName,
},
// NOTE(craigatgoogle): This field is weird, it's required to make the Insert/List calls as a parameter
// named "parent", however it's also defined in the response as an output field called "peering", which
// uses "-" as a delimeter instead of ".". To alleviate user confusion I've opted to model the gcloud
// CLI's approach, calling the field "service" and accepting the same format as the CLI with the "."
// delimiter.
// See: https://cloud.google.com/vpc/docs/configure-private-services-access#creating-connection
"service": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"reserved_peering_ranges": &schema.Schema{
Type: schema.TypeList,
Required: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
},
}
}

func resourceServiceNetworkingConnectionCreate(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)

network := d.Get("network").(string)
serviceNetworkingNetworkName, err := retrieveServiceNetworkingNetworkName(d, config, network)
if err != nil {
return fmt.Errorf("Failed to find Service Networking Connection, err: %s", err)
}

connection := &servicenetworking.Connection{
Network: serviceNetworkingNetworkName,
ReservedPeeringRanges: convertStringArr(d.Get("reserved_peering_ranges").([]interface{})),
}

parentService := formatParentService(d.Get("service").(string))
op, err := config.clientServiceNetworking.Services.Connections.Create(parentService, connection).Do()
if err != nil {
return err
}

if err := serviceNetworkingOperationWait(config, op, "Create Service Networking Connection"); err != nil {
return err
}

connectionId := &connectionId{
Network: network,
Service: d.Get("service").(string),
}

d.SetId(connectionId.Id())
return resourceServiceNetworkingConnectionRead(d, meta)
}

func resourceServiceNetworkingConnectionRead(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)

connectionId, err := parseConnectionId(d.Id())
if err != nil {
return fmt.Errorf("Failed to find Service Networking Connection, err: %s", err)
}

serviceNetworkingNetworkName, err := retrieveServiceNetworkingNetworkName(d, config, connectionId.Network)
if err != nil {
return fmt.Errorf("Failed to find Service Networking Connection, err: %s", err)
}

parentService := formatParentService(connectionId.Service)
listCall := config.clientServiceNetworking.Services.Connections.List(parentService)
listCall.Network(serviceNetworkingNetworkName)
response, err := listCall.Do()
if err != nil {
return err
}

var connection *servicenetworking.Connection
for _, c := range response.Connections {
if c.Network == serviceNetworkingNetworkName {
connection = c
break
}
}

if connection == nil {
return fmt.Errorf("Failed to find Service Networking Connection, network: %s service: %s", connectionId.Network, connectionId.Service)
}

d.Set("network", connectionId.Network)
d.Set("service", connectionId.Service)
d.Set("reserved_peering_ranges", connection.ReservedPeeringRanges)
return nil
}

// NOTE(craigatgoogle): The API for this resource doesn't define an update, however the behavior
// of Create serves as a de facto update by overwriting connections with the duplicate
// tuples: (network/service).
func resourceServiceNetworkingConnectionUpdate(d *schema.ResourceData, meta interface{}) error {
return resourceServiceNetworkingConnectionCreate(d, meta)
}

// NOTE(craigatgoogle): This resource doesn't have a defined Delete method, however an un-documented
// behavior is for the Connection to be deleted when its associated network is deleted. This is
// helpeful for acctest cleanup.
func resourceServiceNetworkingConnectionDelete(d *schema.ResourceData, meta interface{}) error {
connectionId, err := parseConnectionId(d.Id())
if err != nil {
return err
}

log.Printf("[WARNING] Service Networking Connection resources cannot be deleted from GCP. This Connection (network: %s, service: %s) will be removed from Terraform state, but will still be present on the server.", connectionId.Network, connectionId.Service)

d.SetId("")

return nil
}

func resourceServiceNetworkingConnectionImportState(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
connectionId, err := parseConnectionId(d.Id())
if err != nil {
return nil, err
}

d.Set("network", connectionId.Network)
d.Set("service", connectionId.Service)
return []*schema.ResourceData{d}, nil
}

// NOTE(craigatgoogle): The Connection resource in this API doesn't have an Id field, so inorder
// to support the Read method, we create an Id using the tuple(Network, Service).
type connectionId struct {
Network string
Service string
}

func (id *connectionId) Id() string {
return fmt.Sprintf("%s:%s", url.QueryEscape(id.Network), url.QueryEscape(id.Service))
}

func parseConnectionId(id string) (*connectionId, error) {
res := strings.Split(id, ":")

if len(res) != 2 {
return nil, fmt.Errorf("Failed to parse service networking connection id, value: %s", id)
}

network, err := url.QueryUnescape(res[0])
if err != nil {
return nil, fmt.Errorf("Failed to parse service networking connection id, invalid network, err: %s", err)
} else if len(network) == 0 {
return nil, fmt.Errorf("Failed to parse service networking connection id, empty network")
}

service, err := url.QueryUnescape(res[1])
if err != nil {
return nil, fmt.Errorf("Failed to parse service networking connection id, invalid service, err: %s", err)
} else if len(service) == 0 {
return nil, fmt.Errorf("Failed to parse service networking connection id, empty service")
}

return &connectionId{
Network: network,
Service: service,
}, nil
}

// NOTE(craigatgoogle): An out of band aspect of this API is that it uses a unique formatting of network
// different from the standard self_link URI. It requires a call to the resource manager to get the project
// number for the current project.
func retrieveServiceNetworkingNetworkName(d *schema.ResourceData, config *Config, network string) (string, error) {
networkFieldValue, err := ParseNetworkFieldValue(network, d, config)
if err != nil {
return "", fmt.Errorf("Failed to retrieve network field value, err: %s", err)
}

pid := networkFieldValue.Project
if pid == "" {
return "", fmt.Errorf("Could not determine project")
}

project, err := config.clientResourceManager.Projects.Get(pid).Do()
if err != nil {
return "", fmt.Errorf("Failed to retrieve project, pid: %s, err: %s", pid, err)
}

networkName := networkFieldValue.Name
if networkName == "" {
return "", fmt.Errorf("Failed to parse network")
}

// return the network name formatting unique to this API
return fmt.Sprintf("projects/%v/global/networks/%v", project.ProjectNumber, networkName), nil

}

const parentServicePattern = "^services/.+$"

// NOTE(craigatgoogle): An out of band aspect of this API is that it requires the service name to be
// formatted as "services/<serviceName>"
func formatParentService(service string) string {
r := regexp.MustCompile(parentServicePattern)
if !r.MatchString(service) {
return fmt.Sprintf("services/%s", service)
} else {
return service
}
}
92 changes: 92 additions & 0 deletions google-beta/resource_service_networking_connection_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package google

import (
"fmt"
"testing"

"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
)

func TestAccServiceNetworkingConnectionCreate(t *testing.T) {
t.Parallel()

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccServiceNetworkingConnection(
fmt.Sprintf("tf-test-%s", acctest.RandString(10)),
fmt.Sprintf("tf-test-%s", acctest.RandString(10)),
"servicenetworking.googleapis.com",
),
},
resource.TestStep{
ResourceName: "google_service_networking_connection.foobar",
ImportState: true,
ImportStateVerify: true,
},
},
})

}

func TestAccServiceNetworkingConnectionUpdate(t *testing.T) {
t.Parallel()

network := fmt.Sprintf("tf-test-%s", acctest.RandString(10))
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccServiceNetworkingConnection(
network,
fmt.Sprintf("tf-test-%s", acctest.RandString(10)),
"servicenetworking.googleapis.com",
),
},
resource.TestStep{
ResourceName: "google_service_networking_connection.foobar",
ImportState: true,
ImportStateVerify: true,
},
resource.TestStep{
Config: testAccServiceNetworkingConnection(
network,
fmt.Sprintf("tf-test-%s", acctest.RandString(10)),
"servicenetworking.googleapis.com",
),
},
resource.TestStep{
ResourceName: "google_service_networking_connection.foobar",
ImportState: true,
ImportStateVerify: true,
},
},
})

}

func testAccServiceNetworkingConnection(networkName, addressRangeName, serviceName string) string {
return fmt.Sprintf(`
resource "google_compute_network" "foobar" {
name = "%s"
}
resource "google_compute_global_address" "foobar" {
name = "%s"
purpose = "VPC_PEERING"
address_type = "INTERNAL"
prefix_length = 16
network = "${google_compute_network.foobar.self_link}"
}
resource "google_service_networking_connection" "foobar" {
network = "${google_compute_network.foobar.self_link}"
service = "%s"
reserved_peering_ranges = ["${google_compute_global_address.foobar.name}"]
}
`, networkName, addressRangeName, serviceName)
}
Loading

0 comments on commit 7417340

Please sign in to comment.