terraform-provider-google/google/utils.go

339 lines
9.3 KiB
Go
Raw Normal View History

// Contains functions that don't really belong anywhere else.
package google
import (
"fmt"
"log"
"strings"
"time"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
computeBeta "google.golang.org/api/compute/v0.beta"
"google.golang.org/api/compute/v1"
"google.golang.org/api/googleapi"
)
// getRegionFromZone returns the region from a zone for Google cloud.
func getRegionFromZone(zone string) string {
if zone != "" && len(zone) > 2 {
region := zone[:len(zone)-2]
return region
}
return ""
}
// getRegion reads the "region" field from the given resource data and falls
// back to the provider's value if not given. If the provider's value is not
// given, checks for "zone" in either the given resource data or provider,
// and extracts region from zone. If "zone" is not provided, returns an
// error.
func getRegion(d *schema.ResourceData, config *Config) (string, error) {
res, ok := d.GetOk("region")
if !ok {
if config.Region != "" {
return config.Region, nil
}
if zone, err := getZone(d, config); err == nil && getRegionFromZone(zone) != "" {
return getRegionFromZone(zone), nil
}
return "", fmt.Errorf("Cannot determine region: set in this resource, or set provider-level 'region' or 'zone'.")
}
return res.(string), nil
}
// getZone reads the "zone" value from the given resource data and falls back
// to provider's value if not given. If neither is provided, returns an error.
func getZone(d *schema.ResourceData, config *Config) (string, error) {
res, ok := d.GetOk("zone")
if !ok {
if config.Zone != "" {
return config.Zone, nil
}
return "", fmt.Errorf("Cannot determine zone: set in this resource, or set provider-level zone.")
}
return res.(string), nil
}
func getRegionFromInstanceState(is *terraform.InstanceState, config *Config) (string, error) {
res, ok := is.Attributes["region"]
if ok && res != "" {
return res, nil
}
if config.Region != "" {
return config.Region, nil
}
return "", fmt.Errorf("region: required field is not set")
}
// getProject reads the "project" field from the given resource data and falls
// back to the provider's value if not given. If the provider's value is not
// given, an error is returned.
func getProject(d *schema.ResourceData, config *Config) (string, error) {
return getProjectFromSchema("project", d, config)
}
func getProjectFromInstanceState(is *terraform.InstanceState, config *Config) (string, error) {
res, ok := is.Attributes["project"]
if ok && res != "" {
return res, nil
}
if config.Project != "" {
return config.Project, nil
}
return "", fmt.Errorf("project: required field is not set")
}
func getZonalResourceFromRegion(getResource func(string) (interface{}, error), region string, compute *compute.Service, project string) (interface{}, error) {
zoneList, err := compute.Zones.List(project).Do()
if err != nil {
return nil, err
}
var resource interface{}
for _, zone := range zoneList.Items {
if strings.Contains(zone.Name, region) {
resource, err = getResource(zone.Name)
if err != nil {
if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 {
// Resource was not found in this zone
continue
}
return nil, fmt.Errorf("Error reading Resource: %s", err)
}
// Resource was found
return resource, nil
}
}
// Resource does not exist in this region
return nil, nil
}
func getZonalBetaResourceFromRegion(getResource func(string) (interface{}, error), region string, compute *computeBeta.Service, project string) (interface{}, error) {
zoneList, err := compute.Zones.List(project).Do()
if err != nil {
return nil, err
}
var resource interface{}
for _, zone := range zoneList.Items {
if strings.Contains(zone.Name, region) {
resource, err = getResource(zone.Name)
if err != nil {
if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 {
// Resource was not found in this zone
continue
}
return nil, fmt.Errorf("Error reading Resource: %s", err)
}
// Resource was found
return resource, nil
}
}
// Resource does not exist in this region
return nil, nil
}
func getNetworkNameFromSelfLink(network string) (string, error) {
Add Alias IP and Guest Accelerator support to Instance Templates (#639) * Move AliasIpRange helpers into utils To reflect the fact they'll be used by multiple resources. * Pass Config to build helpers, not meta It's the only thing meta is used for. * Refactor getNetwork util methods to return early for the happy path. * Update compute APIs compute.Instance.MinCpuPlatform is now GA. * Fix panic in TestComputeInstanceMigrateState This seemed to be a pre-existing issue, i.e. I could repro it in master. --- FAIL: TestComputeInstanceMigrateState (0.00s) panic: interface conversion: interface {} is nil, not *google.Config [recovered] panic: interface conversion: interface {} is nil, not *google.Config goroutine 85 [running]: testing.tRunner.func1(0xc4205d60f0) /usr/local/Cellar/go/1.9.1/libexec/src/testing/testing.go:711 +0x2d2 panic(0x203acc0, 0xc4205d2080) /usr/local/Cellar/go/1.9.1/libexec/src/runtime/panic.go:491 +0x283 github.com/terraform-providers/terraform-provider-google/google.migrateStateV3toV4(0xc4205f2000, 0x0, 0x0, 0x0, 0x48, 0xc4205f2000) /Users/negz/control/go/src/github.com/terraform-providers/terraform-provider-google/google/resource_compute_instance_migrate.go:182 +0x2405 github.com/terraform-providers/terraform-provider-google/google.resourceComputeInstanceMigrateState(0x2, 0xc4205f2000, 0x0, 0x0, 0x0, 0x0, 0xe0000000000) /Users/negz/control/go/src/github.com/terraform-providers/terraform-provider-google/google/resource_compute_instance_migrate.go:48 +0x21a github.com/terraform-providers/terraform-provider-google/google.runInstanceMigrateTest(0xc4205d60f0, 0x2260816, 0x8, 0x227d23a, 0x20, 0x2, 0xc4205ec0f0, 0xc4205ec120, 0x0, 0x0) /Users/negz/control/go/src/github.com/terraform-providers/terraform-provider-google/google/resource_compute_instance_migrate_test.go:803 +0xc1 github.com/terraform-providers/terraform-provider-google/google.TestComputeInstanceMigrateState(0xc4205d60f0) /Users/negz/control/go/src/github.com/terraform-providers/terraform-provider-google/google/resource_compute_instance_migrate_test.go:71 +0xc84 testing.tRunner(0xc4205d60f0, 0x22d81c0) /usr/local/Cellar/go/1.9.1/libexec/src/testing/testing.go:746 +0xd0 created by testing.(*T).Run /usr/local/Cellar/go/1.9.1/libexec/src/testing/testing.go:789 +0x2de FAIL github.com/terraform-providers/terraform-provider-google/google 0.035s * Use only the v1 API for resource_compute_instance Alias IP ranges, Accelerators, and min CPU platform are now GA. * Move common instance code into utils.go Methods used by both resource_compute_instance and resource_compute_instance_template are currently spread between their respective files, and utils.go. This commit moves them all into utils.go for the sake of consistency. It may be worth considering an instance_common.go file or similar. * Unify compute_instance and compute_instance_template network_interface and service_account code This has the side effect of enabling Alias IP range support for compute_instance_templates. * Add tests for compute instance template Alias IP ranges * Mark instance template region as computed We compute it from the subnet its network interfaces are in. Note this is not new behaviour - I believe it was erroneously missing the computed flag. * Support guest accelerators for instance templates Since most of the code is already there. * Add a test for using 'address' rather than 'network_ip' for instance templates * Don't mark assigned_nat_ip as deprecated * Remove network_interface schema fields that don't make sense for a compute instance template * Add newline after count in instance template docs * Don't try to dedupe guest accelerator expansion code The API calls to Google to create guest accelerators take different values for instances and instance templates. Instance templates don't have a zone and can thus *only* be passed a guest accelerator name. * Use ParseNetworkFieldValue instead of getNetworkLink * Add support for parsing regional fields, and subnetworks specifically Currently unused because subnetworks may have a separate project from that of the instance using them, which complicates looking up the project field. * Fall back to provider region when parsing regional field values Also slightly refactors getXFromSchema field helper functions for readability. * Revert to assigned_nat_ip in compute instance docs * Add beta scaffolding to compute instance and compute instance template Note these resources don't currently use beta features - this is futureproofing. * Fix indentation in comment about instance template alias IP ranges * Consolidate metadata helper functions in metadata.go * Move compute instance (and template) related helpers into their own file
2017-11-28 18:01:27 +00:00
if !strings.HasPrefix(network, "https://www.googleapis.com/compute/") {
return network, nil
}
Add Alias IP and Guest Accelerator support to Instance Templates (#639) * Move AliasIpRange helpers into utils To reflect the fact they'll be used by multiple resources. * Pass Config to build helpers, not meta It's the only thing meta is used for. * Refactor getNetwork util methods to return early for the happy path. * Update compute APIs compute.Instance.MinCpuPlatform is now GA. * Fix panic in TestComputeInstanceMigrateState This seemed to be a pre-existing issue, i.e. I could repro it in master. --- FAIL: TestComputeInstanceMigrateState (0.00s) panic: interface conversion: interface {} is nil, not *google.Config [recovered] panic: interface conversion: interface {} is nil, not *google.Config goroutine 85 [running]: testing.tRunner.func1(0xc4205d60f0) /usr/local/Cellar/go/1.9.1/libexec/src/testing/testing.go:711 +0x2d2 panic(0x203acc0, 0xc4205d2080) /usr/local/Cellar/go/1.9.1/libexec/src/runtime/panic.go:491 +0x283 github.com/terraform-providers/terraform-provider-google/google.migrateStateV3toV4(0xc4205f2000, 0x0, 0x0, 0x0, 0x48, 0xc4205f2000) /Users/negz/control/go/src/github.com/terraform-providers/terraform-provider-google/google/resource_compute_instance_migrate.go:182 +0x2405 github.com/terraform-providers/terraform-provider-google/google.resourceComputeInstanceMigrateState(0x2, 0xc4205f2000, 0x0, 0x0, 0x0, 0x0, 0xe0000000000) /Users/negz/control/go/src/github.com/terraform-providers/terraform-provider-google/google/resource_compute_instance_migrate.go:48 +0x21a github.com/terraform-providers/terraform-provider-google/google.runInstanceMigrateTest(0xc4205d60f0, 0x2260816, 0x8, 0x227d23a, 0x20, 0x2, 0xc4205ec0f0, 0xc4205ec120, 0x0, 0x0) /Users/negz/control/go/src/github.com/terraform-providers/terraform-provider-google/google/resource_compute_instance_migrate_test.go:803 +0xc1 github.com/terraform-providers/terraform-provider-google/google.TestComputeInstanceMigrateState(0xc4205d60f0) /Users/negz/control/go/src/github.com/terraform-providers/terraform-provider-google/google/resource_compute_instance_migrate_test.go:71 +0xc84 testing.tRunner(0xc4205d60f0, 0x22d81c0) /usr/local/Cellar/go/1.9.1/libexec/src/testing/testing.go:746 +0xd0 created by testing.(*T).Run /usr/local/Cellar/go/1.9.1/libexec/src/testing/testing.go:789 +0x2de FAIL github.com/terraform-providers/terraform-provider-google/google 0.035s * Use only the v1 API for resource_compute_instance Alias IP ranges, Accelerators, and min CPU platform are now GA. * Move common instance code into utils.go Methods used by both resource_compute_instance and resource_compute_instance_template are currently spread between their respective files, and utils.go. This commit moves them all into utils.go for the sake of consistency. It may be worth considering an instance_common.go file or similar. * Unify compute_instance and compute_instance_template network_interface and service_account code This has the side effect of enabling Alias IP range support for compute_instance_templates. * Add tests for compute instance template Alias IP ranges * Mark instance template region as computed We compute it from the subnet its network interfaces are in. Note this is not new behaviour - I believe it was erroneously missing the computed flag. * Support guest accelerators for instance templates Since most of the code is already there. * Add a test for using 'address' rather than 'network_ip' for instance templates * Don't mark assigned_nat_ip as deprecated * Remove network_interface schema fields that don't make sense for a compute instance template * Add newline after count in instance template docs * Don't try to dedupe guest accelerator expansion code The API calls to Google to create guest accelerators take different values for instances and instance templates. Instance templates don't have a zone and can thus *only* be passed a guest accelerator name. * Use ParseNetworkFieldValue instead of getNetworkLink * Add support for parsing regional fields, and subnetworks specifically Currently unused because subnetworks may have a separate project from that of the instance using them, which complicates looking up the project field. * Fall back to provider region when parsing regional field values Also slightly refactors getXFromSchema field helper functions for readability. * Revert to assigned_nat_ip in compute instance docs * Add beta scaffolding to compute instance and compute instance template Note these resources don't currently use beta features - this is futureproofing. * Fix indentation in comment about instance template alias IP ranges * Consolidate metadata helper functions in metadata.go * Move compute instance (and template) related helpers into their own file
2017-11-28 18:01:27 +00:00
// extract the network name from SelfLink URL
networkName := network[strings.LastIndex(network, "/")+1:]
if networkName == "" {
return "", fmt.Errorf("network url not valid")
}
return networkName, nil
}
func getRouterLockName(region string, router string) string {
return fmt.Sprintf("router/%s/%s", region, router)
}
func handleNotFoundError(err error, d *schema.ResourceData, resource string) error {
if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 {
log.Printf("[WARN] Removing %s because it's gone", resource)
// The resource doesn't exist anymore
d.SetId("")
return nil
}
return fmt.Errorf("Error reading %s: %s", resource, err)
}
func isConflictError(err error) bool {
if e, ok := err.(*googleapi.Error); ok && e.Code == 409 {
return true
} else if !ok && errwrap.ContainsType(err, &googleapi.Error{}) {
e := errwrap.GetType(err, &googleapi.Error{}).(*googleapi.Error)
if e.Code == 409 {
return true
}
}
return false
}
func linkDiffSuppress(k, old, new string, d *schema.ResourceData) bool {
parts := strings.Split(old, "/")
if parts[len(parts)-1] == new {
return true
}
return false
}
func optionalPrefixSuppress(prefix string) schema.SchemaDiffSuppressFunc {
return func(k, old, new string, d *schema.ResourceData) bool {
return prefix+old == new || prefix+new == old
}
}
func ipCidrRangeDiffSuppress(k, old, new string, d *schema.ResourceData) bool {
// The range may be a:
// A) single IP address (e.g. 10.2.3.4)
// B) CIDR format string (e.g. 10.1.2.0/24)
// C) netmask (e.g. /24)
//
// For A) and B), no diff to suppress, they have to match completely.
// For C), The API picks a network IP address and this creates a diff of the form:
// network_interface.0.alias_ip_range.0.ip_cidr_range: "10.128.1.0/24" => "/24"
// We should only compare the mask portion for this case.
if len(new) > 0 && new[0] == '/' {
oldNetmaskStartPos := strings.LastIndex(old, "/")
if oldNetmaskStartPos != -1 {
oldNetmask := old[strings.LastIndex(old, "/"):]
if oldNetmask == new {
return true
}
}
}
return false
}
// Port range '80' and '80-80' is equivalent.
// `old` is read from the server and always has the full range format (e.g. '80-80', '1024-2048').
// `new` can be either a single port or a port range.
func portRangeDiffSuppress(k, old, new string, d *schema.ResourceData) bool {
if old == new+"-"+new {
return true
}
return false
}
// Single-digit hour is equivalent to hour with leading zero e.g. suppress diff 1:00 => 01:00.
// Assume either value could be in either format.
func rfc3339TimeDiffSuppress(k, old, new string, d *schema.ResourceData) bool {
if (len(old) == 4 && "0"+old == new) || (len(new) == 4 && "0"+new == old) {
return true
}
return false
}
// expandLabels pulls the value of "labels" out of a schema.ResourceData as a map[string]string.
func expandLabels(d *schema.ResourceData) map[string]string {
return expandStringMap(d, "labels")
}
// expandStringMap pulls the value of key out of a schema.ResourceData as a map[string]string.
func expandStringMap(d *schema.ResourceData, key string) map[string]string {
v, ok := d.GetOk(key)
if !ok {
return map[string]string{}
}
return convertStringMap(v.(map[string]interface{}))
}
func convertStringMap(v map[string]interface{}) map[string]string {
m := make(map[string]string)
for k, val := range v {
m[k] = val.(string)
}
return m
}
func convertStringArr(ifaceArr []interface{}) []string {
return convertAndMapStringArr(ifaceArr, func(s string) string { return s })
}
func convertAndMapStringArr(ifaceArr []interface{}, f func(string) string) []string {
var arr []string
for _, v := range ifaceArr {
if v == nil {
continue
}
arr = append(arr, f(v.(string)))
}
return arr
}
func extractLastResourceFromUri(uri string) string {
rUris := strings.Split(uri, "/")
return rUris[len(rUris)-1]
}
func convertStringArrToInterface(strs []string) []interface{} {
arr := make([]interface{}, len(strs))
for i, str := range strs {
arr[i] = str
}
return arr
}
func convertStringSet(set *schema.Set) []string {
s := make([]string, 0, set.Len())
for _, v := range set.List() {
s = append(s, v.(string))
}
return s
}
func mergeSchemas(a, b map[string]*schema.Schema) map[string]*schema.Schema {
merged := make(map[string]*schema.Schema)
for k, v := range a {
merged[k] = v
}
for k, v := range b {
merged[k] = v
}
return merged
}
func retry(retryFunc func() error) error {
return retryTime(retryFunc, 1)
}
func retryTime(retryFunc func() error, minutes int) error {
return resource.Retry(time.Duration(minutes)*time.Minute, func() *resource.RetryError {
err := retryFunc()
if err == nil {
return nil
}
if gerr, ok := err.(*googleapi.Error); ok && (gerr.Code == 429 || gerr.Code == 500 || gerr.Code == 502 || gerr.Code == 503) {
return resource.RetryableError(gerr)
}
return resource.NonRetryableError(err)
})
}
func extractFirstMapConfig(m []interface{}) map[string]interface{} {
if len(m) == 0 {
return map[string]interface{}{}
}
return m[0].(map[string]interface{})
}