Skip to content

Commit

Permalink
Merge pull request #424 from Juniper/rt-validator
Browse files Browse the repository at this point in the history
Improve validation of RT attribute strings
  • Loading branch information
chrismarget-j authored Oct 28, 2023
2 parents 2203c38 + cdb3750 commit 5d9da05
Show file tree
Hide file tree
Showing 4 changed files with 212 additions and 17 deletions.
112 changes: 112 additions & 0 deletions apstra/apstra_validator/parse_rt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package apstravalidator

import (
"context"
"github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"math"
"net"
"strconv"
"strings"
)

const (
rtSep = ":"
rtFormatErr = `A Route Target must take one of the following forms (leading zeros not permitted):
- <2-byte-value>:<4-byte-value>
- <4-byte-value>:<2-byte-value>
- <IPv4-address>:<2-byte-value>
`
)

var _ validator.String = ParseRtValidator{}

type ParseRtValidator struct{}

func (o ParseRtValidator) Description(_ context.Context) string {
return "Ensures that the supplied can be parsed as a Route Target"
}

func (o ParseRtValidator) MarkdownDescription(ctx context.Context) string {
return o.Description(ctx)
}

func (o ParseRtValidator) ValidateString(_ context.Context, req validator.StringRequest, resp *validator.StringResponse) {
if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
return
}

// split the RT string
parts := strings.Split(req.ConfigValue.ValueString(), rtSep)
if len(parts) != 2 {
resp.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic(
req.Path, rtFormatErr, req.ConfigValue.ValueString()))
return
}

var ipFound bool
// check to see if part 1 is an IPv4 address
ip := net.ParseIP(parts[0])
if ip != nil {
ipFound = true // we got an IP!

// is it IPv4?
if len(ip.To4()) != net.IPv4len {
resp.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic(
req.Path, rtFormatErr, req.ConfigValue.ValueString()))
return
}
}

// make sure "part 1" has length and no prepended zeros (if we haven't already decided it's an IPv4 address)
if !ipFound && (len(parts[0]) == 0 || (len(parts[0]) >= 2 && strings.HasPrefix(parts[0], "0"))) {
resp.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic(
req.Path, rtFormatErr, req.ConfigValue.ValueString()))
return
}

// make sure "part 2" has length and no prepended zeros
if len(parts[1]) == 0 || (len(parts[1]) >= 2 && strings.HasPrefix(parts[1], "0")) {
resp.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic(
req.Path, rtFormatErr, req.ConfigValue.ValueString()))
return
}

// determine whether we've got a 32-bit "first part", and thus require a 16-bit "second part"
var firstPartIs32bits bool
if ipFound {
firstPartIs32bits = true
} else {
// try parsing p1 as a 32-bit value
p1, err := strconv.ParseUint(parts[0], 10, 32)
if err != nil {
resp.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic(
req.Path, rtFormatErr, req.ConfigValue.ValueString()))
return
}

// does p1 require 32 bits?
if p1 > math.MaxUint16 {
firstPartIs32bits = true
}
}

// try parsing p2 as a 32-bit value
p2, err := strconv.ParseUint(parts[1], 10, 32)
if err != nil {
resp.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic(
req.Path, rtFormatErr, req.ConfigValue.ValueString()))
return
}

// p1 and p2 can't both be 32 bits
if firstPartIs32bits && p2 > math.MaxUint16 {
resp.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic(
req.Path, rtFormatErr, req.ConfigValue.ValueString()))
return
}
}

func ParseRT() validator.String {
return ParseRtValidator{}
}
77 changes: 77 additions & 0 deletions apstra/apstra_validator/parse_rt_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package apstravalidator

import (
"context"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"testing"
)

func TestParseRtValidator(t *testing.T) {
ctx := context.Background()

validRTs := []string{
"0:0",
"1:1",
"65535:65535",
"65536:65535",
"65535:65536",
"0.0.0.0:0",
"255.255.255.255:0",
"0.0.0.0:65535",
"255.255.255.255:65535",
"4294967295:65535",
"65535:4294967295",
}

invalidRTs := []string{
"",
":",
"1:",
":1",
"1",
"4294967296:65535",
"4294967295:65536",
"65536:4294967295",
"65535:4294967296",
"-1:1",
"1:-1",
"256.1.2.3:1",
"bogus",
}

type testCase struct {
rt string
expectErr bool
}

var testCases []testCase
for _, rt := range validRTs { // load valid test cases
testCases = append(testCases, testCase{rt: rt, expectErr: false})
}
for _, rt := range invalidRTs { // load invalid test cases
testCases = append(testCases, testCase{rt: rt, expectErr: true})
}

for _, tCase := range testCases {
tCase := tCase
t.Run(tCase.rt, func(t *testing.T) {
t.Parallel()
request := validator.StringRequest{
Path: path.Root("test"),
PathExpression: path.MatchRoot("test"),
ConfigValue: types.StringValue(tCase.rt),
}
response := validator.StringResponse{}

ParseRtValidator{}.ValidateString(ctx, request, &response)
if response.Diagnostics.HasError() && !tCase.expectErr {
t.Fail() // error where none expected
}
if !response.Diagnostics.HasError() && tCase.expectErr {
t.Fail() // expected error not found
}
})
}
}
12 changes: 2 additions & 10 deletions apstra/blueprint/datacenter_routing_zone.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,11 +241,7 @@ func (o DatacenterRoutingZone) ResourceAttributes() map[string]resourceSchema.At
ElementType: types.StringType,
Validators: []validator.Set{
setvalidator.SizeAtLeast(1),
setvalidator.ValueStringsAre(
stringvalidator.RegexMatches(
regexp.MustCompile("^[0-9]+:[0-9]+$"),
"import_route_targets must take the form: \"<digits>:<digits>\""),
),
setvalidator.ValueStringsAre(apstravalidator.ParseRT()),
},
},
"export_route_targets": resourceSchema.SetAttribute{
Expand All @@ -254,11 +250,7 @@ func (o DatacenterRoutingZone) ResourceAttributes() map[string]resourceSchema.At
ElementType: types.StringType,
Validators: []validator.Set{
setvalidator.SizeAtLeast(1),
setvalidator.ValueStringsAre(
stringvalidator.RegexMatches(
regexp.MustCompile("^[0-9]+:[0-9]+$"),
"export_route_targets must take the form: \"<digits>:<digits>\""),
),
setvalidator.ValueStringsAre(apstravalidator.ParseRT()),
},
},
}
Expand Down
28 changes: 21 additions & 7 deletions apstra/resource_datacenter_routing_zone_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ package tfapstra

import (
"context"
"encoding/binary"
"errors"
"fmt"
"github.com/Juniper/apstra-go-sdk/apstra"
testutils "github.com/Juniper/terraform-provider-apstra/apstra/test_utils"
"github.com/hashicorp/terraform-plugin-testing/helper/acctest"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"math/rand"
"net"
"strconv"
"strings"
"sync"
Expand All @@ -31,15 +34,26 @@ type routeTargets struct {
rts []string
}

func (o *routeTargets) init(n int) {
s1 := make([]uint32, n)
s2 := make([]uint16, n)
// init loads o.rts with random RT strings
func (o *routeTargets) init(count int) {
s1 := make([]uint32, count)
s2 := make([]uint32, count)
FillWithRandomIntegers(s1)
FillWithRandomIntegers(s2)

o.rts = make([]string, n)
for i := 0; i < n; i++ {
o.rts[i] = fmt.Sprintf("%d:%d", s1[i], s2[i])
o.rts = make([]string, count)
for i := 0; i < count; i++ {
r := rand.Intn(3)
switch r {
case 0: // force to 16-bits:32-bits
o.rts[i] = fmt.Sprintf("%d:%d", uint16(s1[i]), s2[i])
case 1: // force to 32-bits:16-bits
o.rts[i] = fmt.Sprintf("%d:%d", s1[i], uint16(s2[i]))
case 2: // force to IPv4:16-bits
ip := make(net.IP, 4)
binary.BigEndian.PutUint32(ip, s1[i])
o.rts[i] = fmt.Sprintf("%s:%d", ip.String(), uint16(s2[i]))
}
}
}

Expand Down Expand Up @@ -138,7 +152,7 @@ func TestResourceDatacenterRoutingZone_A(t *testing.T) {
rtsB := routeTargets{}

rtsA.init(1)
rtsB.init(3)
rtsB.init(6)

testCases := []testCase{
{
Expand Down

0 comments on commit 5d9da05

Please sign in to comment.