Skip to content

Commit

Permalink
Enable service networking connections in GA
Browse files Browse the repository at this point in the history
Signed-off-by: Modular Magician <[email protected]>
  • Loading branch information
emilymye authored and modular-magician committed Jun 15, 2019
1 parent 6d514d3 commit 9f2b4a7
Show file tree
Hide file tree
Showing 8 changed files with 379 additions and 7 deletions.
13 changes: 13 additions & 0 deletions google/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import (
"google.golang.org/api/pubsub/v1"
runtimeconfig "google.golang.org/api/runtimeconfig/v1beta1"
"google.golang.org/api/servicemanagement/v1"
"google.golang.org/api/servicenetworking/v1"
"google.golang.org/api/serviceusage/v1"
"google.golang.org/api/sourcerepo/v1"
"google.golang.org/api/spanner/v1"
Expand Down Expand Up @@ -164,6 +165,9 @@ type Config struct {
AppEngineBasePath string
clientAppEngine *appengine.APIService

ServiceNetworkingBasePath string
clientServiceNetworking *servicenetworking.APIService

StorageTransferBasePath string
clientStorageTransfer *storagetransfer.Service

Expand Down Expand Up @@ -494,6 +498,15 @@ func (c *Config) LoadAndValidate() error {
c.clientComposer.UserAgent = userAgent
c.clientComposer.BasePath = composerClientBasePath

serviceNetworkingClientBasePath := removeBasePathVersion(c.ServiceNetworkingBasePath)
log.Printf("[INFO] Instantiating Service Networking client for path %s", serviceNetworkingClientBasePath)
c.clientServiceNetworking, err = servicenetworking.NewService(context, option.WithHTTPClient(client))
if err != nil {
return err
}
c.clientServiceNetworking.UserAgent = userAgent
c.clientServiceNetworking.BasePath = serviceNetworkingClientBasePath

storageTransferClientBasePath := removeBasePathVersion(c.StorageTransferBasePath)
log.Printf("[INFO] Instantiating Google Cloud Storage Transfer client for path %s", storageTransferClientBasePath)
c.clientStorageTransfer, err = storagetransfer.NewService(context, option.WithHTTPClient(client))
Expand Down
4 changes: 4 additions & 0 deletions google/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ func Provider() terraform.ResourceProvider {
RuntimeconfigCustomEndpointEntryKey: RuntimeconfigCustomEndpointEntry,
IAMCustomEndpointEntryKey: IAMCustomEndpointEntry,
ServiceManagementCustomEndpointEntryKey: ServiceManagementCustomEndpointEntry,
ServiceNetworkingCustomEndpointEntryKey: ServiceNetworkingCustomEndpointEntry,
ServiceUsageCustomEndpointEntryKey: ServiceUsageCustomEndpointEntry,
BigQueryCustomEndpointEntryKey: BigQueryCustomEndpointEntry,
CloudFunctionsCustomEndpointEntryKey: CloudFunctionsCustomEndpointEntry,
Expand Down Expand Up @@ -270,6 +271,7 @@ func ResourceMapWithErrors() (map[string]*schema.Resource, error) {
"google_kms_crypto_key": resourceKmsCryptoKey(),
"google_kms_crypto_key_iam_binding": ResourceIamBindingWithImport(IamKmsCryptoKeySchema, NewKmsCryptoKeyIamUpdater, CryptoIdParseFunc),
"google_kms_crypto_key_iam_member": ResourceIamMemberWithImport(IamKmsCryptoKeySchema, NewKmsCryptoKeyIamUpdater, CryptoIdParseFunc),
"google_service_networking_connection": resourceServiceNetworkingConnection(),
"google_spanner_instance_iam_binding": ResourceIamBindingWithImport(IamSpannerInstanceSchema, NewSpannerInstanceIamUpdater, SpannerInstanceIdParseFunc),
"google_spanner_instance_iam_member": ResourceIamMemberWithImport(IamSpannerInstanceSchema, NewSpannerInstanceIamUpdater, SpannerInstanceIdParseFunc),
"google_spanner_instance_iam_policy": ResourceIamPolicyWithImport(IamSpannerInstanceSchema, NewSpannerInstanceIamUpdater, SpannerInstanceIdParseFunc),
Expand Down Expand Up @@ -380,6 +382,7 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) {
config.RuntimeconfigBasePath = d.Get(RuntimeconfigCustomEndpointEntryKey).(string)
config.IAMBasePath = d.Get(IAMCustomEndpointEntryKey).(string)
config.ServiceManagementBasePath = d.Get(ServiceManagementCustomEndpointEntryKey).(string)
config.ServiceNetworkingBasePath = d.Get(ServiceNetworkingCustomEndpointEntryKey).(string)
config.ServiceUsageBasePath = d.Get(ServiceUsageCustomEndpointEntryKey).(string)
config.BigQueryBasePath = d.Get(BigQueryCustomEndpointEntryKey).(string)
config.CloudFunctionsBasePath = d.Get(CloudFunctionsCustomEndpointEntryKey).(string)
Expand Down Expand Up @@ -431,6 +434,7 @@ func ConfigureBasePaths(c *Config) {
c.RuntimeconfigBasePath = RuntimeconfigDefaultBasePath
c.IAMBasePath = IAMDefaultBasePath
c.ServiceManagementBasePath = ServiceManagementDefaultBasePath
c.ServiceNetworkingBasePath = ServiceNetworkingDefaultBasePath
c.ServiceUsageBasePath = ServiceUsageDefaultBasePath
c.BigQueryBasePath = BigQueryDefaultBasePath
c.CloudFunctionsBasePath = CloudFunctionsDefaultBasePath
Expand Down
11 changes: 11 additions & 0 deletions google/provider_handwritten_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,17 @@ var ServiceManagementCustomEndpointEntry = &schema.Schema{
}, ServiceManagementDefaultBasePath),
}

var ServiceNetworkingDefaultBasePath = "https://servicenetworking.googleapis.com/v1/"
var ServiceNetworkingCustomEndpointEntryKey = "service_networking_custom_endpoint"
var ServiceNetworkingCustomEndpointEntry = &schema.Schema{
Type: schema.TypeString,
Optional: true,
ValidateFunc: validateCustomEndpoint,
DefaultFunc: schema.MultiEnvDefaultFunc([]string{
"GOOGLE_SERVICE_NETWORKING_CUSTOM_ENDPOINT",
}, ServiceNetworkingDefaultBasePath),
}

var ServiceUsageDefaultBasePath = "https://serviceusage.googleapis.com/v1/"
var ServiceUsageCustomEndpointEntryKey = "service_usage_custom_endpoint"
var ServiceUsageCustomEndpointEntry = &schema.Schema{
Expand Down
234 changes: 233 additions & 1 deletion google/resource_service_networking_connection.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,235 @@
package google

// Magic Modules doesn't let us remove files - blank out beta-only common-compile files for now.
import (
"fmt"
"log"
"net/url"
"regexp"
"strings"

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

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": {
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": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"reserved_peering_ranges": {
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
}
}
Loading

0 comments on commit 9f2b4a7

Please sign in to comment.