Merge "Add base IPAM support"
This commit is contained in:
commit
f9d29a91a1
2
go.mod
2
go.mod
@ -10,7 +10,7 @@ require (
|
||||
github.com/onsi/ginkgo v1.14.2
|
||||
github.com/onsi/gomega v1.10.2
|
||||
github.com/prometheus/common v0.10.0
|
||||
github.com/stretchr/testify v1.6.1 // indirect
|
||||
github.com/stretchr/testify v1.6.1
|
||||
go.uber.org/zap v1.15.0
|
||||
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
|
80
pkg/ipam/errors.go
Normal file
80
pkg/ipam/errors.go
Normal file
@ -0,0 +1,80 @@
|
||||
/*
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package ipam
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ErrSubnetNotAllocated returned if the subnet is not registered in IPAM
|
||||
type ErrSubnetNotAllocated struct {
|
||||
Subnet string
|
||||
}
|
||||
|
||||
// ErrSubnetRangeOverlapsWithExistingRange returned if the subnet's range
|
||||
// overlaps (partially or completely) with an already added range in that subnet
|
||||
type ErrSubnetRangeOverlapsWithExistingRange struct {
|
||||
Subnet string
|
||||
SubnetRange Range
|
||||
}
|
||||
|
||||
// ErrSubnetRangeNotAllocated returned if the subnet's range is not registered in IPAM
|
||||
type ErrSubnetRangeNotAllocated struct {
|
||||
Subnet string
|
||||
SubnetRange Range
|
||||
}
|
||||
|
||||
// ErrSubnetRangeExhausted returned if the subnet's range has no unallocated IPs
|
||||
type ErrSubnetRangeExhausted struct {
|
||||
Subnet string
|
||||
SubnetRange Range
|
||||
}
|
||||
|
||||
// ErrInvalidIPAddress returned if an IP address string is malformed
|
||||
type ErrInvalidIPAddress struct {
|
||||
IP string
|
||||
}
|
||||
|
||||
// ErrNotSupported returned if unsupported address types are used
|
||||
type ErrNotSupported struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e ErrSubnetNotAllocated) Error() string {
|
||||
return fmt.Sprintf("IPAM subnet %s not allocated", e.Subnet)
|
||||
}
|
||||
|
||||
func (e ErrSubnetRangeOverlapsWithExistingRange) Error() string {
|
||||
return fmt.Sprintf("IPAM range [%s,%s] in subnet %s overlaps with an existing range",
|
||||
e.SubnetRange.Start, e.SubnetRange.Stop, e.Subnet)
|
||||
}
|
||||
|
||||
func (e ErrSubnetRangeNotAllocated) Error() string {
|
||||
return fmt.Sprintf("IPAM range [%s,%s] in subnet %s is not allocated",
|
||||
e.SubnetRange.Start, e.SubnetRange.Stop, e.Subnet)
|
||||
}
|
||||
|
||||
func (e ErrSubnetRangeExhausted) Error() string {
|
||||
return fmt.Sprintf("IPAM range [%s,%s] in subnet %s is exhausted",
|
||||
e.SubnetRange.Start, e.SubnetRange.Stop, e.Subnet)
|
||||
}
|
||||
|
||||
func (e ErrInvalidIPAddress) Error() string {
|
||||
return fmt.Sprintf("IP address %s is invalid", e.IP)
|
||||
}
|
||||
|
||||
func (e ErrNotSupported) Error() string {
|
||||
return fmt.Sprintf("%s", e.Message)
|
||||
}
|
219
pkg/ipam/ipam.go
Normal file
219
pkg/ipam/ipam.go
Normal file
@ -0,0 +1,219 @@
|
||||
/*
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package ipam
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
// Ipam provides IPAM reservation, backed by IPPool CRs
|
||||
type Ipam struct {
|
||||
Log logr.Logger
|
||||
Scheme *runtime.Scheme
|
||||
Client client.Client
|
||||
|
||||
ippools map[string]*IPPool
|
||||
}
|
||||
|
||||
// IPPool tracks allocation ranges and statuses within a specific
|
||||
// subnet IPv4 or IPv6 subnet. It has a set of ranges of IPs
|
||||
// within the subnet from which IPs can be allocated by IPAM,
|
||||
// and a set of IPs that are currently allocated already.
|
||||
type IPPool struct {
|
||||
Subnet string
|
||||
Ranges []Range
|
||||
AllocatedIPs []string
|
||||
}
|
||||
|
||||
// Range has (inclusive) bounds within a subnet from which IPs can be allocated
|
||||
type Range struct {
|
||||
Start string
|
||||
Stop string
|
||||
}
|
||||
|
||||
// NewIpam initializes an empty IPAM configuration.
|
||||
// TODO: persist and refresh state from the API server
|
||||
// TODO: add ability to remove IP addresses and ranges
|
||||
func NewIpam() *Ipam {
|
||||
ippools := make(map[string]*IPPool)
|
||||
return &Ipam{
|
||||
ippools: ippools,
|
||||
}
|
||||
}
|
||||
|
||||
// AddSubnetRange adds a range within a subnet for IP allocation
|
||||
// TODO error: invalid range for subnet
|
||||
// TODO error: range overlaps with existing range or subnet overlaps with existing subnet
|
||||
func (i *Ipam) AddSubnetRange(subnet string, subnetRange Range) error {
|
||||
// Does the subnet already exist? (this is fine)
|
||||
ippool, exists := i.ippools[subnet]
|
||||
if !exists {
|
||||
ippool = &IPPool{
|
||||
Subnet: subnet,
|
||||
Ranges: []Range{subnetRange}, // TODO DeepCopy()
|
||||
AllocatedIPs: []string{},
|
||||
}
|
||||
i.ippools[subnet] = ippool
|
||||
} else {
|
||||
// Does the subnet's requested range already exist? (this should fail)
|
||||
exists = false
|
||||
for _, r := range ippool.Ranges {
|
||||
if r == subnetRange {
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if exists {
|
||||
return ErrSubnetRangeOverlapsWithExistingRange{Subnet: subnet, SubnetRange: subnetRange}
|
||||
}
|
||||
}
|
||||
ippool.Ranges = append(ippool.Ranges, subnetRange)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AllocateIP allocates an IP from a range and return it
|
||||
func (i *Ipam) AllocateIP(subnet string, subnetRange Range) (string, error) {
|
||||
// NOTE/TODO: this is not threadsafe, which is fine because
|
||||
// the final impl will use the api server as the backend, not local.
|
||||
ippool, exists := i.ippools[subnet]
|
||||
if !exists {
|
||||
return "", ErrSubnetNotAllocated{Subnet: subnet}
|
||||
}
|
||||
// Make sure the range has been allocated within the subnet
|
||||
var match bool
|
||||
for _, r := range ippool.Ranges {
|
||||
if r == subnetRange {
|
||||
match = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
return "", ErrSubnetRangeNotAllocated{Subnet: subnet, SubnetRange: subnetRange}
|
||||
}
|
||||
|
||||
ip, err := findFreeIPInRange(ippool, subnetRange)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ippool.AllocatedIPs = append(ippool.AllocatedIPs, ip)
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
// This converts IP ranges/addresses into iterable ints,
|
||||
// steps through them looking for one that that is not already
|
||||
// in use, converts it back to a string and returns it.
|
||||
// It does not itself add it to the list of assigned IPs.
|
||||
func findFreeIPInRange(ippool *IPPool, subnetRange Range) (string, error) {
|
||||
allocatedIPSet := sliceToMap(ippool.AllocatedIPs)
|
||||
intToString := intToIPv4String
|
||||
if strings.Contains(ippool.Subnet, ":") {
|
||||
intToString = intToIPv6String
|
||||
}
|
||||
|
||||
// Step through the range looking for free IPs
|
||||
start, err := ipStringToInt(subnetRange.Start)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
stop, err := ipStringToInt(subnetRange.Stop)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for ip := start; ip <= stop; ip++ {
|
||||
_, in := allocatedIPSet[intToString(ip)]
|
||||
if !in {
|
||||
// Found an unallocated IP
|
||||
return intToString(ip), nil
|
||||
}
|
||||
}
|
||||
return "", ErrSubnetRangeExhausted{ippool.Subnet, subnetRange}
|
||||
}
|
||||
|
||||
// Create a map[string]struct{} representation of a string slice,
|
||||
// for efficient set lookups
|
||||
func sliceToMap(slice []string) map[string]struct{} {
|
||||
m := map[string]struct{}{}
|
||||
for _, s := range slice {
|
||||
m[s] = struct{}{}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// Convert an IPV4 or IPV6 address string to an easily iterable uint64.
|
||||
// For IPV4 addresses, this captures the full address (padding the MSB with 0's)
|
||||
// For IPV6 addresses, this captures the most significant 8 bytes,
|
||||
// and excludes the 8-byte interface identifier.
|
||||
func ipStringToInt(ipString string) (uint64, error) {
|
||||
ip := net.ParseIP(ipString)
|
||||
if ip == nil {
|
||||
return 0, ErrInvalidIPAddress{ipString}
|
||||
}
|
||||
|
||||
var bytes []byte
|
||||
if ip.To4() != nil {
|
||||
// IPv4
|
||||
bytes = append(make([]byte, 4), ip.To4()...)
|
||||
} else {
|
||||
// IPv6
|
||||
bytes = ip.To16()[:8]
|
||||
}
|
||||
|
||||
return byteArrayToInt(bytes), nil
|
||||
}
|
||||
|
||||
func intToIPv4String(i uint64) string {
|
||||
bytes := intToByteArray(i)
|
||||
ip := net.IPv4(bytes[4], bytes[5], bytes[6], bytes[7])
|
||||
return ip.String()
|
||||
}
|
||||
|
||||
func intToIPv6String(i uint64) string {
|
||||
// Pad with 8 more bytes of zeros on the right for the hosts's interface,
|
||||
// which will not be determined by IPAM.
|
||||
bytes := append(intToByteArray(i), make([]byte, 8)...)
|
||||
var ip net.IP = bytes
|
||||
return ip.String()
|
||||
}
|
||||
|
||||
// Convert an uint64 into 8 bytes, with most significant byte first
|
||||
// Based on https://gist.github.com/ecoshub/5be18dc63ac64f3792693bb94f00662f
|
||||
func intToByteArray(num uint64) []byte {
|
||||
size := 8
|
||||
arr := make([]byte, size)
|
||||
for i := 0; i < size; i++ {
|
||||
byt := *(*uint8)(unsafe.Pointer(uintptr(unsafe.Pointer(&num)) + uintptr(i)))
|
||||
arr[size-i-1] = byt
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
// Convert an 8-byte array to an uint64
|
||||
// Based on https://gist.github.com/ecoshub/5be18dc63ac64f3792693bb94f00662f
|
||||
func byteArrayToInt(arr []byte) uint64 {
|
||||
val := uint64(0)
|
||||
size := 8
|
||||
for i := 0; i < size; i++ {
|
||||
*(*uint8)(unsafe.Pointer(uintptr(unsafe.Pointer(&val)) + uintptr(size-i-1))) = arr[i]
|
||||
}
|
||||
return val
|
||||
}
|
353
pkg/ipam/ipam_test.go
Normal file
353
pkg/ipam/ipam_test.go
Normal file
@ -0,0 +1,353 @@
|
||||
/*
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package ipam
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAllocateIP(t *testing.T) {
|
||||
tests := []struct {
|
||||
name, subnet, expectedErr string
|
||||
subnetRange Range
|
||||
}{
|
||||
{
|
||||
name: "success ipv4",
|
||||
subnet: "10.0.0.0/16",
|
||||
subnetRange: Range{"10.0.1.0", "10.0.1.9"},
|
||||
},
|
||||
{
|
||||
name: "success ipv6",
|
||||
subnet: "2600:1700:b030:0000::/72",
|
||||
subnetRange: Range{"2600:1700:b030:0000::", "2600:1700:b030:0009::"},
|
||||
},
|
||||
{
|
||||
name: "error subnet not allocated ipv4",
|
||||
subnet: "10.0.0.0/20",
|
||||
subnetRange: Range{"10.0.1.0", "10.0.1.9"},
|
||||
expectedErr: "IPAM subnet 10.0.0.0/20 not allocated",
|
||||
},
|
||||
{
|
||||
name: "error subnet not allocated ipv6",
|
||||
subnet: "2600:1700:b030:0000::/80",
|
||||
subnetRange: Range{"2600:1700:b030:0000::", "2600:1700:b030:0009::"},
|
||||
expectedErr: "IPAM subnet 2600:1700:b030:0000::/80 not allocated",
|
||||
},
|
||||
{
|
||||
name: "error range not allocated ipv4",
|
||||
subnet: "10.0.0.0/16",
|
||||
subnetRange: Range{"10.0.2.0", "10.0.2.9"},
|
||||
expectedErr: "IPAM range [10.0.2.0,10.0.2.9] in subnet 10.0.0.0/16 is not allocated",
|
||||
},
|
||||
{
|
||||
name: "error range not allocated ipv6",
|
||||
subnet: "2600:1700:b030:0000::/72",
|
||||
subnetRange: Range{"2600:1700:b030:0000::", "2600:1700:b030:1111::"},
|
||||
expectedErr: "IPAM range [2600:1700:b030:0000::,2600:1700:b030:1111::] " +
|
||||
"in subnet 2600:1700:b030:0000::/72 is not allocated",
|
||||
},
|
||||
{
|
||||
name: "error range exhausted ipv4",
|
||||
subnet: "192.168.0.0/1",
|
||||
subnetRange: Range{"192.168.0.0", "192.168.0.0"},
|
||||
expectedErr: "IPAM range [192.168.0.0,192.168.0.0] in subnet 192.168.0.0/1 is exhausted",
|
||||
},
|
||||
{
|
||||
name: "error range exhausted ipv6",
|
||||
subnet: "2600:1700:b031:0000::/64",
|
||||
subnetRange: Range{"2600:1700:b031:0000::", "2600:1700:b031:0000::"},
|
||||
expectedErr: "IPAM range [2600:1700:b031:0000::,2600:1700:b031:0000::] " +
|
||||
"in subnet 2600:1700:b031:0000::/64 is exhausted",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ipammer := NewIpam()
|
||||
|
||||
// Pre-populate IPAM with some precondition test data
|
||||
err := ipammer.AddSubnetRange("10.0.0.0/16", Range{"10.0.1.0", "10.0.1.9"})
|
||||
require.NoError(t, err)
|
||||
err = ipammer.AddSubnetRange("2600:1700:b030:0000::/72", Range{"2600:1700:b030:0000::", "2600:1700:b030:0009::"})
|
||||
require.NoError(t, err)
|
||||
err = ipammer.AddSubnetRange("192.168.0.0/1", Range{"192.168.0.0", "192.168.0.0"})
|
||||
require.NoError(t, err)
|
||||
err = ipammer.AddSubnetRange("2600:1700:b031:0000::/64", Range{"2600:1700:b031:0000::", "2600:1700:b031:0000::"})
|
||||
require.NoError(t, err)
|
||||
_, err = ipammer.AllocateIP("192.168.0.0/1", Range{"192.168.0.0", "192.168.0.0"})
|
||||
require.NoError(t, err)
|
||||
_, err = ipammer.AllocateIP("2600:1700:b031:0000::/64", Range{"2600:1700:b031:0000::", "2600:1700:b031:0000::"})
|
||||
require.NoError(t, err)
|
||||
ip, err := ipammer.AllocateIP(tt.subnet, tt.subnetRange)
|
||||
if tt.expectedErr != "" {
|
||||
assert.Equal(t, "", ip)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.expectedErr)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, ip)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test some error handling that is not captured by TestAllocateIP
|
||||
func TestAddSubnetRange(t *testing.T) {
|
||||
tests := []struct {
|
||||
name, subnet, expectedErr string
|
||||
subnetRange Range
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
subnet: "10.0.0.0/16",
|
||||
subnetRange: Range{"10.0.2.0", "10.0.2.9"},
|
||||
expectedErr: "",
|
||||
},
|
||||
{
|
||||
name: "error range already exists",
|
||||
subnet: "10.0.0.0/16",
|
||||
subnetRange: Range{"10.0.1.0", "10.0.1.9"},
|
||||
expectedErr: "IPAM range [10.0.1.0,10.0.1.9] in subnet 10.0.0.0/16 overlaps",
|
||||
},
|
||||
// TODO: check for partially overlapping ranges and subnets
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ipammer := NewIpam()
|
||||
|
||||
// Pre-populate IPAM with some precondition test data
|
||||
err := ipammer.AddSubnetRange("10.0.0.0/16", Range{"10.0.1.0", "10.0.1.9"})
|
||||
require.NoError(t, err)
|
||||
err = ipammer.AddSubnetRange(tt.subnet, tt.subnetRange)
|
||||
if tt.expectedErr != "" {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.expectedErr)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
func TestFindFreeIPInRange(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
subnet string
|
||||
subnetRange Range
|
||||
out string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "ip available IPv4",
|
||||
subnet: "10.0.0.0/16",
|
||||
subnetRange: Range{"10.0.1.0", "10.0.1.10"},
|
||||
out: "10.0.1.0",
|
||||
},
|
||||
{
|
||||
name: "ip unavailable IPv4",
|
||||
subnet: "10.0.0.0/16",
|
||||
subnetRange: Range{"10.0.2.0", "10.0.2.0"},
|
||||
out: "",
|
||||
expectedErr: "IPAM range [10.0.2.0,10.0.2.0] in subnet 10.0.0.0/16 is exhausted",
|
||||
},
|
||||
{
|
||||
name: "ip available IPv6",
|
||||
subnet: "2600:1700:b030:0000::/64",
|
||||
subnetRange: Range{"2600:1700:b030:1001::", "2600:1700:b030:1009::"},
|
||||
out: "2600:1700:b030:1001::",
|
||||
},
|
||||
{
|
||||
name: "ip unavailable IPv6",
|
||||
subnet: "2600:1700:b031::/64",
|
||||
subnetRange: Range{"2600:1700:b031::", "2600:1700:b031::"},
|
||||
expectedErr: "IPAM range [2600:1700:b031::,2600:1700:b031::] " +
|
||||
"in subnet 2600:1700:b031::/64 is exhausted",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ippool := IPPool{
|
||||
Subnet: tt.subnet,
|
||||
// One available and one unavailable range each for ipv4/6
|
||||
Ranges: []Range{
|
||||
{"10.0.1.0", "10.0.1.10"},
|
||||
{"10.0.2.0", "10.0.2.0"},
|
||||
{"2600:1700:b030:1001::", "2600:1700:b030:1009::"},
|
||||
{"2600:1700:b031::", "2600:1700:b031::"},
|
||||
},
|
||||
AllocatedIPs: []string{"10.0.2.0", "2600:1700:b031::"},
|
||||
}
|
||||
actual, err := findFreeIPInRange(&ippool, tt.subnetRange)
|
||||
if tt.expectedErr != "" {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.expectedErr)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.out, actual)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSliceToMap(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in []string
|
||||
out map[string]struct{}
|
||||
}{
|
||||
{
|
||||
name: "empty slice",
|
||||
in: []string{},
|
||||
out: map[string]struct{}{},
|
||||
},
|
||||
{
|
||||
name: "one-element slice",
|
||||
in: []string{"foo"},
|
||||
out: map[string]struct{}{"foo": {}},
|
||||
},
|
||||
{
|
||||
name: "two-element slice",
|
||||
in: []string{"foo", "bar"},
|
||||
out: map[string]struct{}{"foo": {}, "bar": {}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
actual := sliceToMap(tt.in)
|
||||
assert.Equal(t, tt.out, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIPStringToInt(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in string
|
||||
out uint64
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "valid IPv4 address",
|
||||
in: "1.0.0.1",
|
||||
out: uint64(math.Pow(2, 24) + 1),
|
||||
},
|
||||
{
|
||||
name: "invalid IPv4 address",
|
||||
in: "1.0.0.1.1",
|
||||
out: 0,
|
||||
expectedErr: " is invalid",
|
||||
},
|
||||
{
|
||||
name: "valid IPv6 address",
|
||||
in: "0001:0000:0000:0001::",
|
||||
out: uint64(math.Pow(2, 48) + 1),
|
||||
},
|
||||
{
|
||||
name: "invalid IPv6 address",
|
||||
in: "1000:0000:0000:foobar::",
|
||||
out: 0,
|
||||
expectedErr: " is invalid",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
actual, err := ipStringToInt(tt.in)
|
||||
if tt.expectedErr != "" {
|
||||
require.Error(t, err)
|
||||
assert.Empty(t, tt.out)
|
||||
assert.Contains(t, err.Error(), tt.expectedErr)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.out, actual)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntToByteArray(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in uint64
|
||||
out []byte
|
||||
}{
|
||||
{
|
||||
name: "zeros",
|
||||
in: 0,
|
||||
out: make([]byte, 8),
|
||||
},
|
||||
{
|
||||
name: "IPv4 255's",
|
||||
in: uint64(math.Pow(2, 32) - 1),
|
||||
out: []byte{0, 0, 0, 0, 255, 255, 255, 255},
|
||||
},
|
||||
{
|
||||
name: "value in the middle",
|
||||
in: 512,
|
||||
out: []byte{0, 0, 0, 0, 0, 0, 2, 0},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
actual := intToByteArray(tt.in)
|
||||
assert.Equal(t, tt.out, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestByteArrayToInt(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in []byte
|
||||
out uint64
|
||||
}{
|
||||
{
|
||||
name: "zeros",
|
||||
in: make([]byte, 8),
|
||||
out: 0,
|
||||
},
|
||||
{
|
||||
name: "255's",
|
||||
in: []byte{0, 0, 0, 0, 255, 255, 255, 255},
|
||||
out: uint64(math.Pow(2, 32) - 1),
|
||||
},
|
||||
{
|
||||
name: "value in the middle",
|
||||
in: []byte{0, 0, 0, 0, 0, 0, 2, 0},
|
||||
out: 512,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
actual := byteArrayToInt(tt.in)
|
||||
assert.Equal(t, tt.out, actual)
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user