terraform-provider-google/google/utils.go
The Magician 5ce5686d49 Retry instance metadata on fingerprint mismatch. (#3372)
Signed-off-by: Modular Magician <magic-modules@google.com>
2019-04-24 14:01:47 -07:00

469 lines
13 KiB
Go

// 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/(-|<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
}
// 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
}