// Contains functions that don't really belong anywhere else. package google import ( "crypto/sha256" "encoding/hex" "fmt" "log" "net/url" "strings" "time" "github.com/hashicorp/errwrap" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" "google.golang.org/api/googleapi" ) type TerraformResourceData interface { HasChange(string) bool GetOkExists(string) (interface{}, bool) GetOk(string) (interface{}, bool) Get(string) interface{} Set(string, interface{}) error SetId(string) Id() string } // 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 "" } // 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 func getRegion(d TerraformResourceData, config *Config) (string, error) { return getRegionFromSchema("region", "zone", d, config) } // 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 TerraformResourceData, config *Config) (string, error) { return getProjectFromSchema("project", d, config) } // getProjectFromDiff reads the "project" field from the given diff and falls // back to the provider's value if not given. If the provider's value is not // given, an error is returned. func getProjectFromDiff(d *schema.ResourceDiff, config *Config) (string, error) { res, ok := d.GetOk("project") if ok { return res.(string), nil } if config.Project != "" { return config.Project, nil } return "", fmt.Errorf("%s: required field is not set", "project") } 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 isGoogleApiErrorWithCode(err, 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 isGoogleApiErrorWithCode(err error, errCode int) bool { gerr, ok := errwrap.GetType(err, &googleapi.Error{}).(*googleapi.Error) return ok && gerr != nil && gerr.Code == errCode } 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 } 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 } var FINGERPRINT_FAIL_ERRORS = []string{"Invalid fingerprint.", "Supplied fingerprint does not match current metadata fingerprint."} // We've encountered a few common fingerprint-related strings; if this is one of // them, we're confident this is an error due to fingerprints. func isFingerprintError(err error) bool { for _, msg := range FINGERPRINT_FAIL_ERRORS { if strings.Contains(err.Error(), msg) { return true } } return false } 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 { if GetResourceNameFromSelfLink(old) == 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 optionalSurroundingSpacesSuppress(k, old, new string, d *schema.ResourceData) bool { return strings.TrimSpace(old) == strings.TrimSpace(new) } func emptyOrDefaultStringSuppress(defaultVal string) schema.SchemaDiffSuppressFunc { return func(k, old, new string, d *schema.ResourceData) bool { return (old == "" && new == defaultVal) || (new == "" && old == defaultVal) } } 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 } // sha256DiffSuppress // if old is the hex-encoded sha256 sum of new, treat them as equal func sha256DiffSuppress(_, old, new string, _ *schema.ResourceData) bool { return hex.EncodeToString(sha256.New().Sum([]byte(old))) == new } func caseDiffSuppress(_, old, new string, _ *schema.ResourceData) bool { return strings.ToUpper(old) == strings.ToUpper(new) } // 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 TerraformResourceData as a map[string]string. func expandLabels(d TerraformResourceData) map[string]string { return expandStringMap(d, "labels") } // expandEnvironmentVariables pulls the value of "environment_variables" out of a schema.ResourceData as a map[string]string. func expandEnvironmentVariables(d *schema.ResourceData) map[string]string { return expandStringMap(d, "environment_variables") } // expandStringMap pulls the value of key out of a TerraformResourceData as a map[string]string. func expandStringMap(d TerraformResourceData, 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 mapStringArr(original []string, f func(string) string) []string { var arr []string for _, v := range original { arr = append(arr, f(v)) } return arr } 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 golangSetFromStringSlice(strings []string) map[string]struct{} { set := map[string]struct{}{} for _, v := range strings { set[v] = struct{}{} } return set } 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 mergeResourceMaps(ms ...map[string]*schema.Resource) (map[string]*schema.Resource, error) { merged := make(map[string]*schema.Resource) duplicates := []string{} for _, m := range ms { for k, v := range m { if _, ok := merged[k]; ok { duplicates = append(duplicates, k) } merged[k] = v } } var err error if len(duplicates) > 0 { err = fmt.Errorf("saw duplicates in mergeResourceMaps: %v", duplicates) } return merged, err } func retry(retryFunc func() error) error { return retryTime(retryFunc, 1) } func retryTime(retryFunc func() error, minutes int) error { return retryTimeDuration(retryFunc, time.Duration(minutes)*time.Minute) } func retryTimeDuration(retryFunc func() error, duration time.Duration) error { return resource.Retry(duration, func() *resource.RetryError { err := retryFunc() if err == nil { return nil } for _, e := range errwrap.GetAllType(err, &googleapi.Error{}) { if isRetryableError(e) { return resource.RetryableError(e) } } return resource.NonRetryableError(err) }) } func isRetryableError(err error) bool { if gerr, ok := err.(*googleapi.Error); ok && (gerr.Code == 429 || gerr.Code == 500 || gerr.Code == 502 || gerr.Code == 503) { log.Printf("[DEBUG] Dismissed an error as retryable based on error code: %s", err) return true } // These operations are always hitting googleapis.com - they should rarely // time out, and if they do, that timeout is retryable. if urlerr, ok := err.(*url.Error); ok && urlerr.Timeout() { log.Printf("[DEBUG] Dismissed an error as retryable based on googleapis.com target: %s", err) return true } if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 409 && strings.Contains(gerr.Body, "operationInProgress") { // 409's are retried because cloud sql throws a 409 when concurrent calls are made. // The only way right now to determine it is a SQL 409 due to concurrent calls is to // look at the contents of the error message. // See https://github.com/terraform-providers/terraform-provider-google/issues/3279 log.Printf("[DEBUG] Dismissed an error as retryable based on error code 409 and error reason 'operationInProgress': %s", err) return true } if gerr, ok := err.(*googleapi.Error); ok && (gerr.Code == 412) && isFingerprintError(err) { log.Printf("[DEBUG] Dismissed an error as retryable as a fingerprint mismatch: %s", err) return true } return false } func extractFirstMapConfig(m []interface{}) map[string]interface{} { if len(m) == 0 { return map[string]interface{}{} } return m[0].(map[string]interface{}) } func lockedCall(lockKey string, f func() error) error { mutexKV.Lock(lockKey) defer mutexKV.Unlock(lockKey) return f() } // This is a Printf sibling (Nprintf; Named Printf), which handles strings like // Nprintf("Hello %{target}!", map[string]interface{}{"target":"world"}) == "Hello world!". // This is particularly useful for generated tests, where we don't want to use Printf, // since that would require us to generate a very particular ordering of arguments. func Nprintf(format string, params map[string]interface{}) string { for key, val := range params { format = strings.Replace(format, "%{"+key+"}", fmt.Sprintf("%v", val), -1) } return format } // serviceAccountFQN will attempt to generate the fully qualified name in the format of: // "projects/(-|)/serviceAccounts/@.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 } // 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 } func paginatedListRequest(baseUrl string, config *Config, flattener func(map[string]interface{}) []interface{}) ([]interface{}, error) { res, err := sendRequest(config, "GET", baseUrl, nil) if err != nil { return nil, err } ls := flattener(res) pageToken, ok := res["pageToken"] for ok { if pageToken.(string) == "" { break } url := fmt.Sprintf("%s?pageToken=%s", baseUrl, pageToken.(string)) res, err = sendRequest(config, "GET", url, nil) if err != nil { return nil, err } ls = append(ls, flattener(res)) pageToken, ok = res["pageToken"] } return ls, nil }