blob: 477745e0a3303b4774f2905d16660b3b89869e85 [file] [log] [blame]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
// Contains common diff suppress functions.
package tpgresource
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"log"
"net"
"reflect"
"regexp"
"strconv"
"strings"
"time"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)
func OptionalPrefixSuppress(prefix string) schema.SchemaDiffSuppressFunc {
return func(k, old, new string, d *schema.ResourceData) bool {
return prefix+old == new || prefix+new == old
}
}
func IgnoreMissingKeyInMap(key string) schema.SchemaDiffSuppressFunc {
return func(k, old, new string, d *schema.ResourceData) bool {
log.Printf("[DEBUG] - suppressing diff %q with old %q, new %q", k, old, new)
if strings.HasSuffix(k, ".%") {
oldNum, err := strconv.Atoi(old)
if err != nil {
log.Printf("[ERROR] could not parse %q as number, no longer attempting diff suppress", old)
return false
}
newNum, err := strconv.Atoi(new)
if err != nil {
log.Printf("[ERROR] could not parse %q as number, no longer attempting diff suppress", new)
return false
}
return oldNum+1 == newNum
} else if strings.HasSuffix(k, "."+key) {
return old == ""
}
return false
}
}
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 EmptyOrFalseSuppressBoolean(k, old, new string, d *schema.ResourceData) bool {
o, n := d.GetChange(k)
return (o == nil && !n.(bool))
}
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 {
return old == new+"-"+new
}
// 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
}
func EmptyOrUnsetBlockDiffSuppress(k, old, new string, d *schema.ResourceData) bool {
o, n := d.GetChange(strings.TrimSuffix(k, ".#"))
return EmptyOrUnsetBlockDiffSuppressLogic(k, old, new, o, n)
}
// The core logic for EmptyOrUnsetBlockDiffSuppress, in a format that is more conducive
// to unit testing.
func EmptyOrUnsetBlockDiffSuppressLogic(k, old, new string, o, n interface{}) bool {
if !strings.HasSuffix(k, ".#") {
return false
}
var l []interface{}
if old == "0" && new == "1" {
l = n.([]interface{})
} else if new == "0" && old == "1" {
l = o.([]interface{})
} else {
// we don't have one set and one unset, so don't suppress the diff
return false
}
contents, ok := l[0].(map[string]interface{})
if !ok {
return false
}
for _, v := range contents {
if !IsEmptyValue(reflect.ValueOf(v)) {
return false
}
}
return true
}
// Suppress diffs for values that are equivalent except for their use of the words "location"
// compared to "region" or "zone"
func LocationDiffSuppress(k, old, new string, d *schema.ResourceData) bool {
return LocationDiffSuppressHelper(old, new) || LocationDiffSuppressHelper(new, old)
}
func LocationDiffSuppressHelper(a, b string) bool {
return strings.Replace(a, "/locations/", "/regions/", 1) == b ||
strings.Replace(a, "/locations/", "/zones/", 1) == b
}
// For managed SSL certs, if new is an absolute FQDN (trailing '.') but old isn't, treat them as equals.
func AbsoluteDomainSuppress(k, old, new string, _ *schema.ResourceData) bool {
if strings.HasPrefix(k, "managed.0.domains.") {
return old == strings.TrimRight(new, ".") || new == strings.TrimRight(old, ".")
}
return false
}
func TimestampDiffSuppress(format string) schema.SchemaDiffSuppressFunc {
return func(_, old, new string, _ *schema.ResourceData) bool {
oldT, err := time.Parse(format, old)
if err != nil {
return false
}
newT, err := time.Parse(format, new)
if err != nil {
return false
}
return oldT == newT
}
}
// Suppresses diff for IPv4 and IPv6 different formats.
// It also suppresses diffs if an IP is changing to a reference.
func InternalIpDiffSuppress(_, old, new string, _ *schema.ResourceData) bool {
addr_equality := false
netmask_equality := false
addr_netmask_old := strings.Split(old, "/")
addr_netmask_new := strings.Split(new, "/")
// Check if old or new are IPs (with or without netmask)
var addr_old net.IP
if net.ParseIP(addr_netmask_old[0]) == nil {
addr_old = net.ParseIP(old)
} else {
addr_old = net.ParseIP(addr_netmask_old[0])
}
var addr_new net.IP
if net.ParseIP(addr_netmask_new[0]) == nil {
addr_new = net.ParseIP(new)
} else {
addr_new = net.ParseIP(addr_netmask_new[0])
}
if addr_old != nil {
if addr_new == nil {
// old is an IP and new is a reference
addr_equality = true
} else {
// old and new are IP addresses
addr_equality = bytes.Equal(addr_old, addr_new)
}
}
// If old and new both have a netmask compare them, otherwise suppress
// This is not technically correct but prevents the permadiff described in https://github.com/hashicorp/terraform-provider-google/issues/16400
if (len(addr_netmask_old)) == 2 && (len(addr_netmask_new) == 2) {
netmask_equality = addr_netmask_old[1] == addr_netmask_new[1]
} else {
netmask_equality = true
}
return addr_equality && netmask_equality
}
// Suppress diffs for duration format. ex "60.0s" and "60s" same
// https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#duration
func DurationDiffSuppress(k, old, new string, d *schema.ResourceData) bool {
oDuration, err := time.ParseDuration(old)
if err != nil {
return false
}
nDuration, err := time.ParseDuration(new)
if err != nil {
return false
}
return oDuration == nDuration
}
// Use this method when the field accepts either an IP address or a
// self_link referencing a resource (such as google_compute_route's
// next_hop_ilb)
func CompareIpAddressOrSelfLinkOrResourceName(_, old, new string, _ *schema.ResourceData) bool {
// if we can parse `new` as an IP address, then compare as strings
if net.ParseIP(new) != nil {
return new == old
}
// otherwise compare as self links
return CompareSelfLinkOrResourceName("", old, new, nil)
}
// Suppress all diffs, used for Disk.Interface which is a nonfunctional field
func AlwaysDiffSuppress(_, _, _ string, _ *schema.ResourceData) bool {
return true
}
// Use this method when subnet is optioanl and auto_create_subnetworks = true
// API sometimes choose a subnet so the diff needs to be ignored
func CompareOptionalSubnet(_, old, new string, _ *schema.ResourceData) bool {
if IsEmptyValue(reflect.ValueOf(new)) {
return true
}
// otherwise compare as self links
return CompareSelfLinkOrResourceName("", old, new, nil)
}
// Suppress diffs in below cases
// "https://hello-rehvs75zla-uc.a.run.app/" -> "https://hello-rehvs75zla-uc.a.run.app"
// "https://hello-rehvs75zla-uc.a.run.app" -> "https://hello-rehvs75zla-uc.a.run.app/"
func LastSlashDiffSuppress(_, old, new string, _ *schema.ResourceData) bool {
if last := len(new) - 1; last >= 0 && new[last] == '/' {
new = new[:last]
}
if last := len(old) - 1; last >= 0 && old[last] == '/' {
old = old[:last]
}
return new == old
}
// Suppress diffs when the value read from api
// has the project number instead of the project name
func ProjectNumberDiffSuppress(_, old, new string, _ *schema.ResourceData) bool {
var a2, b2 string
reN := regexp.MustCompile("projects/\\d+")
re := regexp.MustCompile("projects/[^/]+")
replacement := []byte("projects/equal")
a2 = string(reN.ReplaceAll([]byte(old), replacement))
b2 = string(re.ReplaceAll([]byte(new), replacement))
return a2 == b2
}
func IsNewResource(diff TerraformResourceDiff) bool {
name := diff.Get("name")
return name.(string) == ""
}
func CompareCryptoKeyVersions(_, old, new string, _ *schema.ResourceData) bool {
// The API can return cryptoKeyVersions even though it wasn't specified.
// format: projects/<project>/locations/<region>/keyRings/<keyring>/cryptoKeys/<key>/cryptoKeyVersions/1
kmsKeyWithoutVersions := strings.Split(old, "/cryptoKeyVersions")[0]
if kmsKeyWithoutVersions == new {
return true
}
return false
}
func CidrOrSizeDiffSuppress(k, old, new string, d *schema.ResourceData) bool {
// If the user specified a size and the API returned a full cidr block, suppress.
return strings.HasPrefix(new, "/") && strings.HasSuffix(old, new)
}