mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-28 05:57:25 +00:00
Add dry-run support to the IP allocator subsystem
This commit is contained in:
parent
237434bd42
commit
52856f3fbe
@ -37,6 +37,9 @@ type Interface interface {
|
|||||||
CIDR() net.IPNet
|
CIDR() net.IPNet
|
||||||
IPFamily() api.IPFamily
|
IPFamily() api.IPFamily
|
||||||
Has(ip net.IP) bool
|
Has(ip net.IP) bool
|
||||||
|
|
||||||
|
// DryRun offers a way to try operations without persisting them.
|
||||||
|
DryRun() Interface
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -46,11 +49,12 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ErrNotInRange struct {
|
type ErrNotInRange struct {
|
||||||
|
IP net.IP
|
||||||
ValidRange string
|
ValidRange string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *ErrNotInRange) Error() string {
|
func (e *ErrNotInRange) Error() string {
|
||||||
return fmt.Sprintf("provided IP is not in the valid range. The range of valid IPs is %s", e.ValidRange)
|
return fmt.Sprintf("the provided IP (%v) is not in the valid range. The range of valid IPs is %s", e.IP, e.ValidRange)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Range is a contiguous block of IPs that can be allocated atomically.
|
// Range is a contiguous block of IPs that can be allocated atomically.
|
||||||
@ -98,11 +102,13 @@ func New(cidr *net.IPNet, allocatorFactory allocator.AllocatorFactory) (*Range,
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
family = api.IPv4Protocol
|
family = api.IPv4Protocol
|
||||||
// Don't use the IPv4 network's broadcast address.
|
// Don't use the IPv4 network's broadcast address, but don't just
|
||||||
|
// Allocate() it - we don't ever want to be able to release it.
|
||||||
max--
|
max--
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't use the network's ".0" address.
|
// Don't use the network's ".0" address, but don't just Allocate() it - we
|
||||||
|
// don't ever want to be able to release it.
|
||||||
base.Add(base, big.NewInt(1))
|
base.Add(base, big.NewInt(1))
|
||||||
max--
|
max--
|
||||||
|
|
||||||
@ -114,6 +120,7 @@ func New(cidr *net.IPNet, allocatorFactory allocator.AllocatorFactory) (*Range,
|
|||||||
}
|
}
|
||||||
var err error
|
var err error
|
||||||
r.alloc, err = allocatorFactory(r.max, rangeSpec)
|
r.alloc, err = allocatorFactory(r.max, rangeSpec)
|
||||||
|
|
||||||
return &r, err
|
return &r, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,18 +169,35 @@ func (r *Range) CIDR() net.IPNet {
|
|||||||
return *r.net
|
return *r.net
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DryRun returns a non-persisting form of this Range.
|
||||||
|
func (r *Range) DryRun() Interface {
|
||||||
|
return dryRunRange{r}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For clearer code.
|
||||||
|
const dryRunTrue = true
|
||||||
|
const dryRunFalse = false
|
||||||
|
|
||||||
// Allocate attempts to reserve the provided IP. ErrNotInRange or
|
// Allocate attempts to reserve the provided IP. ErrNotInRange or
|
||||||
// ErrAllocated will be returned if the IP is not valid for this range
|
// ErrAllocated will be returned if the IP is not valid for this range
|
||||||
// or has already been reserved. ErrFull will be returned if there
|
// or has already been reserved. ErrFull will be returned if there
|
||||||
// are no addresses left.
|
// are no addresses left.
|
||||||
func (r *Range) Allocate(ip net.IP) error {
|
func (r *Range) Allocate(ip net.IP) error {
|
||||||
|
return r.allocate(ip, dryRunFalse)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Range) allocate(ip net.IP, dryRun bool) error {
|
||||||
label := r.CIDR()
|
label := r.CIDR()
|
||||||
ok, offset := r.contains(ip)
|
ok, offset := r.contains(ip)
|
||||||
if !ok {
|
if !ok {
|
||||||
// update metrics
|
// update metrics
|
||||||
clusterIPAllocationErrors.WithLabelValues(label.String()).Inc()
|
clusterIPAllocationErrors.WithLabelValues(label.String()).Inc()
|
||||||
|
return &ErrNotInRange{ip, r.net.String()}
|
||||||
return &ErrNotInRange{r.net.String()}
|
}
|
||||||
|
if dryRun {
|
||||||
|
// Don't bother to check whether the IP is actually free. It's racy and
|
||||||
|
// not worth the effort to plumb any further.
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
allocated, err := r.alloc.Allocate(offset)
|
allocated, err := r.alloc.Allocate(offset)
|
||||||
@ -200,7 +224,17 @@ func (r *Range) Allocate(ip net.IP) error {
|
|||||||
// AllocateNext reserves one of the IPs from the pool. ErrFull may
|
// AllocateNext reserves one of the IPs from the pool. ErrFull may
|
||||||
// be returned if there are no addresses left.
|
// be returned if there are no addresses left.
|
||||||
func (r *Range) AllocateNext() (net.IP, error) {
|
func (r *Range) AllocateNext() (net.IP, error) {
|
||||||
|
return r.allocateNext(dryRunFalse)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Range) allocateNext(dryRun bool) (net.IP, error) {
|
||||||
label := r.CIDR()
|
label := r.CIDR()
|
||||||
|
if dryRun {
|
||||||
|
// Don't bother finding a free value. It's racy and not worth the
|
||||||
|
// effort to plumb any further.
|
||||||
|
return r.CIDR().IP, nil
|
||||||
|
}
|
||||||
|
|
||||||
offset, ok, err := r.alloc.AllocateNext()
|
offset, ok, err := r.alloc.AllocateNext()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// update metrics
|
// update metrics
|
||||||
@ -226,10 +260,17 @@ func (r *Range) AllocateNext() (net.IP, error) {
|
|||||||
// unallocated IP or an IP out of the range is a no-op and
|
// unallocated IP or an IP out of the range is a no-op and
|
||||||
// returns no error.
|
// returns no error.
|
||||||
func (r *Range) Release(ip net.IP) error {
|
func (r *Range) Release(ip net.IP) error {
|
||||||
|
return r.release(ip, dryRunFalse)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Range) release(ip net.IP, dryRun bool) error {
|
||||||
ok, offset := r.contains(ip)
|
ok, offset := r.contains(ip)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if dryRun {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
err := r.alloc.Release(offset)
|
err := r.alloc.Release(offset)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@ -312,3 +353,40 @@ func (r *Range) contains(ip net.IP) (bool, int) {
|
|||||||
func calculateIPOffset(base *big.Int, ip net.IP) int {
|
func calculateIPOffset(base *big.Int, ip net.IP) int {
|
||||||
return int(big.NewInt(0).Sub(netutils.BigForIP(ip), base).Int64())
|
return int(big.NewInt(0).Sub(netutils.BigForIP(ip), base).Int64())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dryRunRange is a shim to satisfy Interface without persisting state.
|
||||||
|
type dryRunRange struct {
|
||||||
|
real *Range
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dry dryRunRange) Allocate(ip net.IP) error {
|
||||||
|
return dry.real.allocate(ip, dryRunTrue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dry dryRunRange) AllocateNext() (net.IP, error) {
|
||||||
|
return dry.real.allocateNext(dryRunTrue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dry dryRunRange) Release(ip net.IP) error {
|
||||||
|
return dry.real.release(ip, dryRunTrue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dry dryRunRange) ForEach(cb func(net.IP)) {
|
||||||
|
dry.real.ForEach(cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dry dryRunRange) CIDR() net.IPNet {
|
||||||
|
return dry.real.CIDR()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dry dryRunRange) IPFamily() api.IPFamily {
|
||||||
|
return dry.real.IPFamily()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dry dryRunRange) DryRun() Interface {
|
||||||
|
return dry
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dry dryRunRange) Has(ip net.IP) bool {
|
||||||
|
return dry.real.Has(ip)
|
||||||
|
}
|
||||||
|
@ -76,34 +76,34 @@ func TestAllocate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
t.Logf("base: %v", r.base.Bytes())
|
t.Logf("base: %v", r.base.Bytes())
|
||||||
if f := r.Free(); f != tc.free {
|
if f := r.Free(); f != tc.free {
|
||||||
t.Errorf("Test %s unexpected free %d", tc.name, f)
|
t.Errorf("[%s] wrong free: expected %d, got %d", tc.name, tc.free, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
rCIDR := r.CIDR()
|
rCIDR := r.CIDR()
|
||||||
if rCIDR.String() != tc.cidr {
|
if rCIDR.String() != tc.cidr {
|
||||||
t.Errorf("allocator returned a different cidr")
|
t.Errorf("[%s] wrong CIDR: expected %v, got %v", tc.name, tc.cidr, rCIDR.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.IPFamily() != tc.family {
|
if r.IPFamily() != tc.family {
|
||||||
t.Errorf("allocator returned wrong IP family")
|
t.Errorf("[%s] wrong IP family: expected %v, got %v", tc.name, tc.family, r.IPFamily())
|
||||||
}
|
}
|
||||||
|
|
||||||
if f := r.Used(); f != 0 {
|
if f := r.Used(); f != 0 {
|
||||||
t.Errorf("Test %s unexpected used %d", tc.name, f)
|
t.Errorf("[%s]: wrong used: expected %d, got %d", tc.name, 0, f)
|
||||||
}
|
}
|
||||||
found := sets.NewString()
|
found := sets.NewString()
|
||||||
count := 0
|
count := 0
|
||||||
for r.Free() > 0 {
|
for r.Free() > 0 {
|
||||||
ip, err := r.AllocateNext()
|
ip, err := r.AllocateNext()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %s error @ %d: %v", tc.name, count, err)
|
t.Fatalf("[%s] error @ %d: %v", tc.name, count, err)
|
||||||
}
|
}
|
||||||
count++
|
count++
|
||||||
if !cidr.Contains(ip) {
|
if !cidr.Contains(ip) {
|
||||||
t.Fatalf("Test %s allocated %s which is outside of %s", tc.name, ip, cidr)
|
t.Fatalf("[%s] allocated %s which is outside of %s", tc.name, ip, cidr)
|
||||||
}
|
}
|
||||||
if found.Has(ip.String()) {
|
if found.Has(ip.String()) {
|
||||||
t.Fatalf("Test %s allocated %s twice @ %d", tc.name, ip, count)
|
t.Fatalf("[%s] allocated %s twice @ %d", tc.name, ip, count)
|
||||||
}
|
}
|
||||||
found.Insert(ip.String())
|
found.Insert(ip.String())
|
||||||
}
|
}
|
||||||
@ -116,17 +116,17 @@ func TestAllocate(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if f := r.Free(); f != 1 {
|
if f := r.Free(); f != 1 {
|
||||||
t.Errorf("Test %s unexpected free %d", tc.name, f)
|
t.Errorf("[%s] wrong free: expected %d, got %d", tc.name, 1, f)
|
||||||
}
|
}
|
||||||
if f := r.Used(); f != (tc.free - 1) {
|
if f := r.Used(); f != (tc.free - 1) {
|
||||||
t.Errorf("Test %s unexpected free %d", tc.name, f)
|
t.Errorf("[%s] wrong free: expected %d, got %d", tc.name, tc.free-1, f)
|
||||||
}
|
}
|
||||||
ip, err := r.AllocateNext()
|
ip, err := r.AllocateNext()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if !released.Equal(ip) {
|
if !released.Equal(ip) {
|
||||||
t.Errorf("Test %s unexpected %s : %s", tc.name, ip, released)
|
t.Errorf("[%s] unexpected %s : %s", tc.name, ip, released)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := r.Release(released); err != nil {
|
if err := r.Release(released); err != nil {
|
||||||
@ -142,19 +142,19 @@ func TestAllocate(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if f := r.Free(); f != 1 {
|
if f := r.Free(); f != 1 {
|
||||||
t.Errorf("Test %s unexpected free %d", tc.name, f)
|
t.Errorf("[%s] wrong free: expected %d, got %d", tc.name, 1, f)
|
||||||
}
|
}
|
||||||
if f := r.Used(); f != (tc.free - 1) {
|
if f := r.Used(); f != (tc.free - 1) {
|
||||||
t.Errorf("Test %s unexpected free %d", tc.name, f)
|
t.Errorf("[%s] wrong free: expected %d, got %d", tc.name, tc.free-1, f)
|
||||||
}
|
}
|
||||||
if err := r.Allocate(released); err != nil {
|
if err := r.Allocate(released); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if f := r.Free(); f != 0 {
|
if f := r.Free(); f != 0 {
|
||||||
t.Errorf("Test %s unexpected free %d", tc.name, f)
|
t.Errorf("[%s] wrong free: expected %d, got %d", tc.name, 0, f)
|
||||||
}
|
}
|
||||||
if f := r.Used(); f != tc.free {
|
if f := r.Used(); f != tc.free {
|
||||||
t.Errorf("Test %s unexpected free %d", tc.name, f)
|
t.Errorf("[%s] wrong free: expected %d, got %d", tc.name, tc.free, f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -514,3 +514,67 @@ func expectMetrics(t *testing.T, label string, em testMetrics) {
|
|||||||
t.Fatalf("metrics error: expected %v, received %v", em, m)
|
t.Fatalf("metrics error: expected %v, received %v", em, m)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDryRun(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
cidr string
|
||||||
|
family api.IPFamily
|
||||||
|
}{{
|
||||||
|
name: "IPv4",
|
||||||
|
cidr: "192.168.1.0/24",
|
||||||
|
family: api.IPv4Protocol,
|
||||||
|
}, {
|
||||||
|
name: "IPv6",
|
||||||
|
cidr: "2001:db8:1::/48",
|
||||||
|
family: api.IPv6Protocol,
|
||||||
|
}}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
_, cidr, err := netutils.ParseCIDRSloppy(tc.cidr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected failure: %v", err)
|
||||||
|
}
|
||||||
|
r, err := NewInMemory(cidr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected failure: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
baseUsed := r.Used()
|
||||||
|
|
||||||
|
rCIDR := r.DryRun().CIDR()
|
||||||
|
if rCIDR.String() != tc.cidr {
|
||||||
|
t.Errorf("allocator returned a different cidr")
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.DryRun().IPFamily() != tc.family {
|
||||||
|
t.Errorf("allocator returned wrong IP family")
|
||||||
|
}
|
||||||
|
|
||||||
|
expectUsed := func(t *testing.T, r *Range, expect int) {
|
||||||
|
t.Helper()
|
||||||
|
if u := r.Used(); u != expect {
|
||||||
|
t.Errorf("unexpected used count: got %d, wanted %d", u, expect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expectUsed(t, r, baseUsed)
|
||||||
|
|
||||||
|
err = r.DryRun().Allocate(netutils.AddIPOffset(netutils.BigForIP(cidr.IP), 1))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected failure: %v", err)
|
||||||
|
}
|
||||||
|
expectUsed(t, r, baseUsed)
|
||||||
|
|
||||||
|
_, err = r.DryRun().AllocateNext()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected failure: %v", err)
|
||||||
|
}
|
||||||
|
expectUsed(t, r, baseUsed)
|
||||||
|
|
||||||
|
if err := r.DryRun().Release(cidr.IP); err != nil {
|
||||||
|
t.Fatalf("unexpected failure: %v", err)
|
||||||
|
}
|
||||||
|
expectUsed(t, r, baseUsed)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user