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
50 changes: 35 additions & 15 deletions internal/registry/ipam/tableconvertor/columns.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package tableconvertor

import (
"fmt"
"math/big"
"sort"
"strings"

Expand Down Expand Up @@ -36,17 +37,16 @@ func IPPools() rest.TableConvertor {
nameColumn,
{Name: "CIDR", Type: "string", Description: "Address range the pool holds."},
{Name: "Family", Type: "string", Description: "Address family of the pool."},
{Name: "Classes", Type: "string", Description: "Classes this pool offers itself to."},
{Name: "Class", Type: "string", Description: "Class this pool serves: the classes it offers itself to, or the class that provisioned it."},
{Name: "Utilization", Type: "string", Description: "Allocated share of the pool's address space."},
ageColumn,
{Name: "Parent", Type: "string", Priority: wide, Description: "Pool this one was carved from."},
{Name: "Class", Type: "string", Priority: wide, Description: "Class that provisioned this pool, if any."},
{Name: "Scope", Type: "string", Priority: wide, Description: "References this pool exists for."},
{Name: "Phase", Type: "string", Priority: wide, Description: "Current lifecycle phase."},
}, func(obj runtime.Object, name, age string) []interface{} {
p, ok := obj.(*ipam.IPPool)
if !ok {
return []interface{}{name, "", "", "", "", age, "", "", "", ""}
return []interface{}{name, "", "", "", "", age, "", "", ""}
}
// The carved range on a provisioned pool, the declared one on a root.
cidr := p.Status.AllocatedCIDR
Expand All @@ -61,11 +61,10 @@ func IPPools() rest.TableConvertor {
name,
orEmpty(cidr),
orEmpty(string(family)),
orEmpty(strings.Join(p.Spec.ClassNames, ",")),
percent(p.Status.UtilizationPercent),
poolClass(p),
poolUtilization(p),
age,
localRefName(p.Spec.ParentPoolRef),
localRefName(p.Spec.ClassRef),
formatScope(p.Spec.Scope),
orEmpty(string(p.Status.Phase)),
}
Expand Down Expand Up @@ -214,18 +213,39 @@ func formatScope(scope map[string]ipam.ScopeRef) string {
return strings.Join(cells, " ")
}

// percent renders utilization to one decimal place. A pool that has handed out
// a /64 from a /32 is not empty, so a value that rounds to zero but is not zero
// prints as "<0.1%" rather than claiming the pool is untouched.
func percent(v float64) string {
switch {
case v <= 0:
// poolClass names the class a pool serves. Exactly one of these is set on any
// given pool: an operator-authored pool lists the classes it offers itself to,
// and a cascade-provisioned pool names the class that carved it. Reading only
// the first would print "<none>" for every provisioned pool, which is most of
// them.
func poolClass(p *ipam.IPPool) string {
if len(p.Spec.ClassNames) > 0 {
return strings.Join(p.Spec.ClassNames, ",")
}
return localRefName(p.Spec.ClassRef)
}

// poolUtilization renders the allocated share of a pool.
//
// status.utilizationPercent is rounded to four decimal places, and an IPv6 pool
// is big enough that a real allocation rounds to zero: a /48 carved from a /32
// is 0.0000%. Whether the pool has been drawn on at all is therefore read from
// the capacity, which is exact, and the percentage is only used for how much.
func poolUtilization(p *ipam.IPPool) string {
if !allocatedAny(p.Status.Capacity.Allocated) {
return "0%"
case v < 0.05:
}
if p.Status.UtilizationPercent < 0.05 {
return "<0.1%"
default:
return fmt.Sprintf("%.1f%%", v)
}
return fmt.Sprintf("%.1f%%", p.Status.UtilizationPercent)
}

// allocatedAny reports whether a capacity count is a non-zero number. The counts
// are decimal strings because an IPv6 pool overflows every integer type.
func allocatedAny(s string) bool {
n, ok := new(big.Int).SetString(s, 10)
return ok && n.Sign() > 0
}

func orEmpty(s string) string {
Expand Down
79 changes: 66 additions & 13 deletions internal/registry/ipam/tableconvertor/tableconvertor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ func TestDefaultColumnsAreTheIdentifyingOnes(t *testing.T) {
obj runtime.Object
want []string
}{
{"ippools", IPPools(), &ipam.IPPool{}, []string{"Name", "CIDR", "Family", "Classes", "Utilization", "Age"}},
{"ippools", IPPools(), &ipam.IPPool{}, []string{"Name", "CIDR", "Family", "Class", "Utilization", "Age"}},
{"ipclaims", IPClaims(), &ipam.IPClaim{}, []string{"Name", "Class", "Family", "Allocated", "Pool", "Phase", "Age"}},
{"ipallocations", IPAllocations(), &ipam.IPAllocation{}, []string{"Name", "Allocated", "Pool", "Class", "Purpose", "Phase", "Age"}},
{"ipclasses", IPClasses(), &ipam.IPClass{}, []string{"Name", "Family", "Parent", "Prefix", "Pool Per", "Phase", "Age"}},
Expand Down Expand Up @@ -186,22 +186,75 @@ func TestListCarriesPaginationState(t *testing.T) {
}
}

// A pool holding one /64 out of a /32 is not empty. Rounding it to 0% would
// say the pool is untouched when it is not.
func TestPercentDistinguishesEmptyFromNearlyEmpty(t *testing.T) {
// status.utilizationPercent is rounded to four decimal places, so a real
// allocation out of an IPv6 pool arrives here as a literal 0. Reading emptiness
// off that number reports every fabric-identity and subnet pool in staging as
// untouched when they are not; the capacity counts are exact and settle it.
func TestUtilizationReadsEmptinessFromCapacity(t *testing.T) {
for _, tc := range []struct {
in float64
name string
pool ipam.IPPool
want string
}{
{0, "0%"},
{0.0000001, "<0.1%"},
{0.04, "<0.1%"},
{12.34, "12.3%"},
{100, "100.0%"},
{
name: "never drawn on",
pool: ipam.IPPool{Status: ipam.IPPoolStatus{
Capacity: ipam.PoolCapacity{Allocated: "0", Total: "18446744073709551616"},
}},
want: "0%",
},
{
name: "drawn on, but rounds to zero: a /48 out of a /32",
pool: ipam.IPPool{Status: ipam.IPPoolStatus{
UtilizationPercent: 0,
Capacity: ipam.PoolCapacity{Allocated: "1033017668127734890496", Total: "79228162514264337593543950336"},
}},
want: "<0.1%",
},
{
name: "measurable",
pool: ipam.IPPool{Status: ipam.IPPoolStatus{
UtilizationPercent: 12.34,
Capacity: ipam.PoolCapacity{Allocated: "64", Total: "512"},
}},
want: "12.3%",
},
{
name: "full",
pool: ipam.IPPool{Status: ipam.IPPoolStatus{
UtilizationPercent: 100,
Capacity: ipam.PoolCapacity{Allocated: "512", Total: "512"},
}},
want: "100.0%",
},
{
name: "no capacity reported at all",
pool: ipam.IPPool{Status: ipam.IPPoolStatus{}},
want: "0%",
},
} {
if got := percent(tc.in); got != tc.want {
t.Errorf("percent(%v) = %q, want %q", tc.in, got, tc.want)
}
t.Run(tc.name, func(t *testing.T) {
if got := poolUtilization(&tc.pool); got != tc.want {
t.Errorf("poolUtilization = %q, want %q", got, tc.want)
}
})
}
}

// Exactly one of classNames and classRef is set on any given pool. Printing
// only classNames showed "<none>" for every cascade-provisioned pool, which in
// staging is nearly all of them.
func TestClassColumnIsPopulatedOnBothKindsOfPool(t *testing.T) {
operator := &ipam.IPPool{Spec: ipam.IPPoolSpec{ClassNames: []string{"datum-fabric-identity"}}}
if got := poolClass(operator); got != "datum-fabric-identity" {
t.Errorf("operator-authored pool class = %q, want the class it offers itself to", got)
}
provisioned := &ipam.IPPool{Spec: ipam.IPPoolSpec{ClassRef: &ipam.LocalRef{Name: "datum-subnet-ipv6"}}}
if got := poolClass(provisioned); got != "datum-subnet-ipv6" {
t.Errorf("provisioned pool class = %q, want the class that carved it", got)
}
if got := poolClass(&ipam.IPPool{}); got != "<none>" {
t.Errorf("pool with neither = %q, want <none>", got)
}
}

Expand Down
Loading