Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions vultr/resource_vultr_firewall_rule.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,12 @@ func resourceVultrFirewallRule() *schema.Resource {
ValidateFunc: validation.StringInSlice([]string{"icmp", "tcp", "udp", "gre", "ah", "esp"}, false),
},
"subnet": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validation.IsIPAddress,
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validation.IsIPAddress,
DiffSuppressFunc: suppressIPDiff,
StateFunc: canonicalizeIP,
},
"subnet_size": {
Type: schema.TypeInt,
Expand Down
29 changes: 29 additions & 0 deletions vultr/utility.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import (
"encoding/json"
"fmt"
"net"

Check failure on line 6 in vultr/utility.go

View workflow job for this annotation

GitHub Actions / Golangci-Lint

File is not properly formatted (gofmt)
"net/http"
"strings"

Expand Down Expand Up @@ -70,3 +71,31 @@
func IgnoreCase(k, old, new string, d *schema.ResourceData) bool {
return strings.EqualFold(old, new)
}

// suppressIPDiff returns true when old and new are the same IP address
// just written differently (leading zeros, mixed case, etc).
// Handles both v4 and v6 transparently.
func suppressIPDiff(_, old, new string, _ *schema.ResourceData) bool {
oldIP := net.ParseIP(old)
newIP := net.ParseIP(new)

// if either side doesn't parse, fall through to normal string compare
if oldIP == nil || newIP == nil {
return old == new
}

return oldIP.Equal(newIP)
}

// canonicalizeIP parses and re-renders an IP address to its canonical
// string form. For v6 this strips leading zeros and lowercases per
// RFC 5952, matching what the Vultr API returns.
// If it can't parse (bad input or empty string), just pass through.
func canonicalizeIP(val interface{}) string {
raw := val.(string)
ip := net.ParseIP(raw)
if ip == nil {
return raw
}
return ip.String()
}
129 changes: 129 additions & 0 deletions vultr/utility_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package vultr

import (
"testing"
)

func TestSuppressIPDiff(t *testing.T) {
cases := []struct {
name string
old string
new string
suppress bool
}{
// leading zero in v6 group
{
name: "v6 leading zero in group",
old: "2001:db8:1000:3b79:5400:5ff:fedf:fade",
new: "2001:db8:1000:3b79:5400:05ff:fedf:fade",
suppress: true,
},
// same addr both sides -- no-op
{
name: "v6 identical",
old: "2001:db8::1",
new: "2001:db8::1",
suppress: true,
},
// full expansion vs compressed -- same addr
{
name: "v6 expanded vs compressed",
old: "2001:db8:0:0:0:0:0:1",
new: "2001:db8::1",
suppress: true,
},
// actually different addrs
{
name: "v6 different addrs",
old: "2001:db8::1",
new: "2001:db8::2",
suppress: false,
},
// v4 sanity -- should be a noop but make sure it doesn't break
{
name: "v4 identical",
old: "10.0.0.1",
new: "10.0.0.1",
suppress: true,
},
{
name: "v4 different",
old: "10.0.0.1",
new: "10.0.0.2",
suppress: false,
},
// garbage in -- fall through to string compare
{
name: "unparseable falls through",
old: "not-an-ip",
new: "also-not-an-ip",
suppress: false,
},
// mixed case hex -- same addr
{
name: "v6 mixed case",
old: "2001:db8:1000:3b79:5400:5FF:FEDF:FADE",
new: "2001:db8:1000:3b79:5400:5ff:fedf:fade",
suppress: true,
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := suppressIPDiff("subnet", tc.old, tc.new, nil)
if got != tc.suppress {
t.Errorf("suppressIPDiff(%q, %q) = %v, want %v",
tc.old, tc.new, got, tc.suppress)
}
})
}
}

func TestCanonicalizeIP(t *testing.T) {
cases := []struct {
name string
in string
want string
}{
// the actual bug case
{
name: "strips leading zero from v6 group",
in: "2001:db8:1000:3b79:5400:05ff:fedf:fade",
want: "2001:db8:1000:3b79:5400:5ff:fedf:fade",
},
// already canonical -- passthrough
{
name: "already canonical v6",
in: "2001:db8::1",
want: "2001:db8::1",
},
// v4 passthrough
{
name: "v4 passthrough",
in: "10.0.0.1",
want: "10.0.0.1",
},
// full form gets compressed
{
name: "v6 full form compresses",
in: "2001:0db8:0000:0000:0000:0000:0000:0001",
want: "2001:db8::1",
},
// bad input -- pass through unchanged
{
name: "bad input passthrough",
in: "not-an-ip",
want: "not-an-ip",
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := canonicalizeIP(tc.in)
if got != tc.want {
t.Errorf("canonicalizeIP(%q) = %q, want %q",
tc.in, got, tc.want)
}
})
}
}
Loading