Skip to content
Open
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
60 changes: 60 additions & 0 deletions pkg/state/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ limitations under the License.
package state

import (
"os"
"strconv"
"strings"

"github.com/nmstate/kubernetes-nmstate/api/shared"
Expand Down Expand Up @@ -220,13 +222,71 @@ func isUnmanaged(ifaceData map[string]interface{}) bool {
return ifaceData["state"] == "ignore"
}

const (
// defaultInterfaceCountThreshold is the default number of interfaces
// above which verbose fields are stripped from VLAN interfaces to
// reduce the NodeNetworkState object size and avoid exceeding the
// etcd request size limit (1.5 MB). Can be overridden via the
// VLAN_FILTER_INTERFACE_COUNT_THRESHOLD environment variable.
defaultInterfaceCountThreshold = 500

// vlanInterfaceType is the nmstate type string for VLAN interfaces.
vlanInterfaceType = "vlan"
)

// getInterfaceCountThreshold returns the interface count threshold,
// checking the VLAN_FILTER_INTERFACE_COUNT_THRESHOLD environment variable
// first, falling back to the default value.
func getInterfaceCountThreshold() int {
if val, ok := os.LookupEnv("VLAN_FILTER_INTERFACE_COUNT_THRESHOLD"); ok {
if n, err := strconv.Atoi(val); err == nil && n > 0 {
return n
}
}
return defaultInterfaceCountThreshold
}

// vlanEssentialFields is the set of fields to preserve on VLAN interfaces
// when the interface count exceeds the threshold. All other fields are
// stripped to reduce serialized size.
var vlanEssentialFields = map[string]struct{}{
"name": {},
"type": {},
"state": {},
"ipv4": {},
"ipv6": {},
"vlan": {},
}

// stripVerboseVlanFields removes non-essential fields from VLAN interfaces
// when the total interface count exceeds the threshold. This prevents the
// NodeNetworkState from exceeding the etcd request size limit on nodes
// with large numbers of VLANs.
func stripVerboseVlanFields(interfaces []interfaceState) []interfaceState {
if len(interfaces) <= getInterfaceCountThreshold() {
return interfaces
}
for i := range interfaces {
if interfaces[i].Type != vlanInterfaceType {
continue
}
for key := range interfaces[i].Data {
if _, keep := vlanEssentialFields[key]; !keep {
delete(interfaces[i].Data, key)
}
}
}
return interfaces
}

func filterOut(currentState shared.State) (shared.State, error) {
var state rootState
if err := yaml.Unmarshal(currentState.Raw, &state); err != nil {
return currentState, err
}

state.Interfaces = filterOutInterfaces(state.Interfaces)
state.Interfaces = stripVerboseVlanFields(state.Interfaces)
if state.Routes != nil {
state.Routes.Running = filterOutRoutes(state.Routes.Running, state.Interfaces)
state.Routes.Config = filterOutRoutes(state.Routes.Config, state.Interfaces)
Expand Down
165 changes: 165 additions & 0 deletions pkg/state/filter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,14 @@ limitations under the License.
package state

import (
"fmt"
"os"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

nmstate "github.com/nmstate/kubernetes-nmstate/api/shared"
"sigs.k8s.io/yaml"
)

var _ = Describe("FilterOut", func() {
Expand Down Expand Up @@ -389,6 +393,167 @@ routes:
Expect(returnedState).To(MatchYAML(filteredState))
})
})

Context("when the number of interfaces exceeds the threshold", func() {
It("should strip verbose fields from VLAN interfaces but keep essential ones", func() {
// Build a state with more than defaultInterfaceCountThreshold interfaces
// by generating VLAN interfaces
yamlStr := "interfaces:\n"
yamlStr += "- name: eth0\n state: up\n type: ethernet\n mtu: 1500\n mac-address: 00:11:22:33:44:55\n"
for i := 0; i < defaultInterfaceCountThreshold+10; i++ {
yamlStr += fmt.Sprintf(`- name: eth0.%d
type: vlan
state: up
mtu: 1400
mac-address: 02:00:00:%02x:%02x:00
ipv4:
enabled: true
address:
- ip: 192.168.%d.1
prefix-length: 24
vlan:
id: %d
base-iface: eth0
lldp:
enabled: false
ethtool:
feature:
tx-checksum-ip-generic: true
`, i, i/256, i%256, i%256, i)
}
yamlStr += "routes:\n config: []\n running: []\n"

state := nmstate.NewState(yamlStr)
result, err := filterOut(state)
Expect(err).ToNot(HaveOccurred())

// Parse the result to verify
var parsed rootState
err = yaml.Unmarshal(result.Raw, &parsed)
Expect(err).ToNot(HaveOccurred())

// Ethernet interface should be untouched
eth0 := parsed.Interfaces[0]
Expect(eth0.Name).To(Equal("eth0"))
Expect(eth0.Data).To(HaveKey("mtu"))
Expect(eth0.Data).To(HaveKey("mac-address"))

// VLAN interfaces should have verbose fields stripped
vlan0 := parsed.Interfaces[1]
Expect(vlan0.Name).To(Equal("eth0.0"))
Expect(vlan0.Type).To(Equal("vlan"))
Expect(vlan0.Data).To(HaveKey("state"))
Expect(vlan0.Data).To(HaveKey("vlan"))
Expect(vlan0.Data).To(HaveKey("ipv4"))
// Verbose fields should be gone
Expect(vlan0.Data).NotTo(HaveKey("mtu"))
Expect(vlan0.Data).NotTo(HaveKey("mac-address"))
Expect(vlan0.Data).NotTo(HaveKey("lldp"))
Expect(vlan0.Data).NotTo(HaveKey("ethtool"))
})

It("should not strip fields when interface count is below threshold", func() {
state := nmstate.NewState(`
interfaces:
- name: eth0
state: up
type: ethernet
mtu: 1500
- name: eth0.100
type: vlan
state: up
mtu: 1400
mac-address: 02:00:00:00:64:00
ipv4:
enabled: true
vlan:
id: 100
base-iface: eth0
lldp:
enabled: false
routes:
config: []
running: []
`)
result, err := filterOut(state)
Expect(err).ToNot(HaveOccurred())

var parsed rootState
err = yaml.Unmarshal(result.Raw, &parsed)
Expect(err).ToNot(HaveOccurred())

// VLAN should retain all fields when below threshold
vlan := parsed.Interfaces[1]
Expect(vlan.Data).To(HaveKey("mtu"))
Expect(vlan.Data).To(HaveKey("mac-address"))
Expect(vlan.Data).To(HaveKey("lldp"))
})
})

Context("when the VLAN_FILTER_INTERFACE_COUNT_THRESHOLD env var is set", func() {
AfterEach(func() {
os.Unsetenv("VLAN_FILTER_INTERFACE_COUNT_THRESHOLD")
})

It("should use the env var value as the threshold", func() {
os.Setenv("VLAN_FILTER_INTERFACE_COUNT_THRESHOLD", "1")

// With threshold=1, even 2 interfaces should trigger stripping
state := nmstate.NewState(`
interfaces:
- name: eth0
state: up
type: ethernet
mtu: 1500
mac-address: 00:11:22:33:44:55
- name: eth0.100
type: vlan
state: up
mtu: 1400
mac-address: 02:00:00:00:64:00
ipv4:
enabled: true
vlan:
id: 100
base-iface: eth0
lldp:
enabled: false
routes:
config: []
running: []
`)
result, err := filterOut(state)
Expect(err).ToNot(HaveOccurred())

var parsed rootState
err = yaml.Unmarshal(result.Raw, &parsed)
Expect(err).ToNot(HaveOccurred())

// VLAN should have verbose fields stripped
vlan := parsed.Interfaces[1]
Expect(vlan.Data).NotTo(HaveKey("mtu"))
Expect(vlan.Data).NotTo(HaveKey("mac-address"))
Expect(vlan.Data).NotTo(HaveKey("lldp"))
// Essential fields preserved
Expect(vlan.Data).To(HaveKey("state"))
Expect(vlan.Data).To(HaveKey("vlan"))
Expect(vlan.Data).To(HaveKey("ipv4"))
})

It("should fall back to default when env var is invalid", func() {
os.Setenv("VLAN_FILTER_INTERFACE_COUNT_THRESHOLD", "not-a-number")

Expect(getInterfaceCountThreshold()).To(Equal(defaultInterfaceCountThreshold))
})

It("should fall back to default when env var is zero or negative", func() {
os.Setenv("VLAN_FILTER_INTERFACE_COUNT_THRESHOLD", "0")
Expect(getInterfaceCountThreshold()).To(Equal(defaultInterfaceCountThreshold))

os.Setenv("VLAN_FILTER_INTERFACE_COUNT_THRESHOLD", "-5")
Expect(getInterfaceCountThreshold()).To(Equal(defaultInterfaceCountThreshold))
})
})
})

var _ = Describe("CountRoutes", func() {
Expand Down
Loading