2017-08-15 23:35:46 +00:00
|
|
|
// Contains functions that don't really belong anywhere else.
|
|
|
|
|
|
|
|
package google
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"log"
|
|
|
|
"strings"
|
2017-10-03 19:41:04 +00:00
|
|
|
"time"
|
2017-08-15 23:35:46 +00:00
|
|
|
|
|
|
|
"github.com/hashicorp/errwrap"
|
2017-10-03 19:41:04 +00:00
|
|
|
"github.com/hashicorp/terraform/helper/resource"
|
2017-08-15 23:35:46 +00:00
|
|
|
"github.com/hashicorp/terraform/helper/schema"
|
2017-09-07 17:38:26 +00:00
|
|
|
"github.com/hashicorp/terraform/terraform"
|
2017-08-15 23:35:46 +00:00
|
|
|
computeBeta "google.golang.org/api/compute/v0.beta"
|
|
|
|
"google.golang.org/api/compute/v1"
|
|
|
|
"google.golang.org/api/googleapi"
|
|
|
|
)
|
|
|
|
|
2018-05-09 18:24:40 +00:00
|
|
|
type TerraformResourceData interface {
|
|
|
|
HasChange(string) bool
|
|
|
|
GetOk(string) (interface{}, bool)
|
|
|
|
Set(string, interface{}) error
|
|
|
|
SetId(string)
|
|
|
|
Id() string
|
|
|
|
}
|
|
|
|
|
2017-08-15 23:35:46 +00:00
|
|
|
// 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 ""
|
|
|
|
}
|
|
|
|
|
2018-01-09 21:57:02 +00:00
|
|
|
// Infers the region based on the following (in order of priority):
|
|
|
|
// - `region` field in resource schema
|
|
|
|
// - region extracted from the `zone` field in resource schema
|
|
|
|
// - provider-level region
|
|
|
|
// - region extracted from the provider-level zone
|
2018-02-02 18:55:43 +00:00
|
|
|
func getRegion(d TerraformResourceData, config *Config) (string, error) {
|
2018-01-09 21:57:02 +00:00
|
|
|
return getRegionFromSchema("region", "zone", d, config)
|
2017-12-06 22:30:04 +00:00
|
|
|
}
|
|
|
|
|
2017-09-07 17:38:26 +00:00
|
|
|
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")
|
|
|
|
}
|
|
|
|
|
2017-08-15 23:35:46 +00:00
|
|
|
// 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.
|
2018-02-02 18:55:43 +00:00
|
|
|
func getProject(d TerraformResourceData, config *Config) (string, error) {
|
2017-10-10 16:53:57 +00:00
|
|
|
return getProjectFromSchema("project", d, config)
|
2017-08-15 23:35:46 +00:00
|
|
|
}
|
|
|
|
|
2017-09-07 17:38:26 +00:00
|
|
|
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")
|
|
|
|
}
|
|
|
|
|
2017-08-15 23:35:46 +00:00
|
|
|
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 getRouterLockName(region string, router string) string {
|
|
|
|
return fmt.Sprintf("router/%s/%s", region, router)
|
|
|
|
}
|
|
|
|
|
|
|
|
func handleNotFoundError(err error, d *schema.ResourceData, resource string) error {
|
2018-03-27 23:41:44 +00:00
|
|
|
if isGoogleApiErrorWithCode(err, 404) {
|
2017-08-15 23:35:46 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2018-03-27 23:41:44 +00:00
|
|
|
func isGoogleApiErrorWithCode(err error, errCode int) bool {
|
|
|
|
gerr, ok := errwrap.GetType(err, &googleapi.Error{}).(*googleapi.Error)
|
|
|
|
return ok && gerr != nil && gerr.Code == errCode
|
|
|
|
}
|
|
|
|
|
2018-05-22 23:59:33 +00:00
|
|
|
func isApiNotEnabledError(err error) bool {
|
|
|
|
gerr, ok := errwrap.GetType(err, &googleapi.Error{}).(*googleapi.Error)
|
|
|
|
if !ok {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
if gerr == nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
if gerr.Code != 403 {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
for _, e := range gerr.Errors {
|
|
|
|
if e.Reason == "accessNotConfigured" {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2018-06-22 23:21:35 +00:00
|
|
|
func isFailedPreconditionError(err error) bool {
|
|
|
|
gerr, ok := errwrap.GetType(err, &googleapi.Error{}).(*googleapi.Error)
|
|
|
|
if !ok {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
if gerr == nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
if gerr.Code != 400 {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
for _, e := range gerr.Errors {
|
|
|
|
if e.Reason == "failedPrecondition" {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2017-08-15 23:35:46 +00:00
|
|
|
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 {
|
2018-01-17 18:45:28 +00:00
|
|
|
if GetResourceNameFromSelfLink(old) == new {
|
2017-08-15 23:35:46 +00:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2017-10-03 16:26:19 +00:00
|
|
|
func optionalPrefixSuppress(prefix string) schema.SchemaDiffSuppressFunc {
|
|
|
|
return func(k, old, new string, d *schema.ResourceData) bool {
|
|
|
|
return prefix+old == new || prefix+new == old
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-20 16:49:50 +00:00
|
|
|
func optionalSurroundingSpacesSuppress(k, old, new string, d *schema.ResourceData) bool {
|
|
|
|
return strings.TrimSpace(old) == strings.TrimSpace(new)
|
|
|
|
}
|
|
|
|
|
2018-01-31 22:36:03 +00:00
|
|
|
func emptyOrDefaultStringSuppress(defaultVal string) schema.SchemaDiffSuppressFunc {
|
|
|
|
return func(k, old, new string, d *schema.ResourceData) bool {
|
|
|
|
return (old == "" && new == defaultVal) || (new == "" && old == defaultVal)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-07 20:43:00 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2018-05-14 17:07:59 +00:00
|
|
|
func caseDiffSuppress(_, old, new string, _ *schema.ResourceData) bool {
|
|
|
|
return strings.ToUpper(old) == strings.ToUpper(new)
|
|
|
|
}
|
|
|
|
|
2017-10-31 18:28:55 +00:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2017-11-13 19:30:26 +00:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2017-08-18 20:34:11 +00:00
|
|
|
// 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 {
|
2017-10-03 20:14:51 +00:00
|
|
|
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)
|
2017-08-18 20:34:11 +00:00
|
|
|
}
|
2017-10-03 20:14:51 +00:00
|
|
|
return m
|
2017-08-18 20:34:11 +00:00
|
|
|
}
|
|
|
|
|
2017-08-15 23:35:46 +00:00
|
|
|
func convertStringArr(ifaceArr []interface{}) []string {
|
2017-10-30 23:41:37 +00:00
|
|
|
return convertAndMapStringArr(ifaceArr, func(s string) string { return s })
|
|
|
|
}
|
|
|
|
|
|
|
|
func convertAndMapStringArr(ifaceArr []interface{}, f func(string) string) []string {
|
2017-08-15 23:35:46 +00:00
|
|
|
var arr []string
|
|
|
|
for _, v := range ifaceArr {
|
|
|
|
if v == nil {
|
|
|
|
continue
|
|
|
|
}
|
2017-10-30 23:41:37 +00:00
|
|
|
arr = append(arr, f(v.(string)))
|
2017-08-15 23:35:46 +00:00
|
|
|
}
|
|
|
|
return arr
|
|
|
|
}
|
2017-09-09 00:02:32 +00:00
|
|
|
|
2017-10-05 20:20:16 +00:00
|
|
|
func convertStringArrToInterface(strs []string) []interface{} {
|
|
|
|
arr := make([]interface{}, len(strs))
|
|
|
|
for i, str := range strs {
|
|
|
|
arr[i] = str
|
|
|
|
}
|
|
|
|
return arr
|
|
|
|
}
|
|
|
|
|
2017-09-09 00:02:32 +00:00
|
|
|
func convertStringSet(set *schema.Set) []string {
|
|
|
|
s := make([]string, 0, set.Len())
|
|
|
|
for _, v := range set.List() {
|
|
|
|
s = append(s, v.(string))
|
|
|
|
}
|
|
|
|
return s
|
|
|
|
}
|
2017-09-13 02:36:07 +00:00
|
|
|
|
2017-10-04 00:09:34 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2018-05-17 23:33:30 +00:00
|
|
|
func mergeResourceMaps(ms ...map[string]*schema.Resource) map[string]*schema.Resource {
|
2018-04-30 21:30:43 +00:00
|
|
|
merged := make(map[string]*schema.Resource)
|
|
|
|
|
2018-05-17 23:33:30 +00:00
|
|
|
for _, m := range ms {
|
|
|
|
for k, v := range m {
|
|
|
|
merged[k] = v
|
|
|
|
}
|
2018-04-30 21:30:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return merged
|
|
|
|
}
|
|
|
|
|
2017-10-03 19:41:04 +00:00
|
|
|
func retry(retryFunc func() error) error {
|
2017-11-14 19:41:57 +00:00
|
|
|
return retryTime(retryFunc, 1)
|
|
|
|
}
|
|
|
|
|
|
|
|
func retryTime(retryFunc func() error, minutes int) error {
|
|
|
|
return resource.Retry(time.Duration(minutes)*time.Minute, func() *resource.RetryError {
|
2017-10-03 19:41:04 +00:00
|
|
|
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)
|
|
|
|
})
|
|
|
|
}
|
2017-11-21 17:32:43 +00:00
|
|
|
|
|
|
|
func extractFirstMapConfig(m []interface{}) map[string]interface{} {
|
|
|
|
if len(m) == 0 {
|
|
|
|
return map[string]interface{}{}
|
|
|
|
}
|
|
|
|
|
|
|
|
return m[0].(map[string]interface{})
|
|
|
|
}
|
2018-02-05 17:45:53 +00:00
|
|
|
|
|
|
|
func lockedCall(lockKey string, f func() error) error {
|
|
|
|
mutexKV.Lock(lockKey)
|
|
|
|
defer mutexKV.Unlock(lockKey)
|
|
|
|
|
|
|
|
return f()
|
|
|
|
}
|
2018-06-01 00:31:45 +00:00
|
|
|
|
|
|
|
// serviceAccountFQN will attempt to generate the fully qualified name in the format of:
|
2018-06-18 20:37:41 +00:00
|
|
|
// "projects/(-|<project>)/serviceAccounts/<service_account_id>@<project>.iam.gserviceaccount.com"
|
|
|
|
// A project is required if we are trying to build the FQN from a service account id and
|
|
|
|
// and error will be returned in this case if no project is set in the resource or the
|
|
|
|
// provider-level config
|
|
|
|
func serviceAccountFQN(serviceAccount string, d TerraformResourceData, config *Config) (string, error) {
|
|
|
|
// If the service account id is already the fully qualified name
|
|
|
|
if strings.HasPrefix(serviceAccount, "projects/") {
|
|
|
|
return serviceAccount, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the service account id is an email
|
|
|
|
if strings.Contains(serviceAccount, "@") {
|
|
|
|
return "projects/-/serviceAccounts/" + serviceAccount, nil
|
2018-06-01 00:31:45 +00:00
|
|
|
}
|
2018-06-18 20:37:41 +00:00
|
|
|
|
|
|
|
// Get the project from the resource or fallback to the project
|
|
|
|
// in the provider configuration
|
|
|
|
project, err := getProject(d, config)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
return fmt.Sprintf("projects/-/serviceAccounts/%s@%s.iam.gserviceaccount.com", serviceAccount, project), nil
|
2018-06-01 00:31:45 +00:00
|
|
|
}
|