From c576f988f1fc3847b27f9bc1c890a098066459fd Mon Sep 17 00:00:00 2001 From: aryanghai12 Date: Wed, 5 Aug 2026 12:17:40 +0530 Subject: [PATCH 1/3] fix(containerprofile): make MaxTsProfileSize account bytes consistently ReportSyscall added mapset.Append's return (element count, 0 or 1) straight into the byte-size accumulator, so syscalls contributed ~nothing toward MaxTsProfileSize. ReportNetworkEvent also sized only the raw NetworkEvent, missing the Identifier/DNS/selector fields createNetworkNeighbor adds at serialization - both let the pre-send estimate undercount, so profiles kept growing past storage's own cap instead of flushing early. Fixes #870. Signed-off-by: aryanghai12 --- .gitignore | 3 +- .../v1/event_reporting.go | 19 ++++- .../v1/event_reporting_test.go | 71 ++++++++++++++++++- 3 files changed, 89 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 135c3206a7..d56d707255 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ temp resources/ebpf/falco/* node-agent __pycache__ -tracers.tar \ No newline at end of file +tracers.tar +LFX_AGENT_SANDBOX_PREP.md \ No newline at end of file diff --git a/pkg/containerprofilemanager/v1/event_reporting.go b/pkg/containerprofilemanager/v1/event_reporting.go index 077875fe1a..c8e985b017 100644 --- a/pkg/containerprofilemanager/v1/event_reporting.go +++ b/pkg/containerprofilemanager/v1/event_reporting.go @@ -19,6 +19,15 @@ import ( var procRegex = regexp.MustCompile(`^/proc/\d+`) +// networkNeighborExpansionEstimate approximates, in bytes, how much a NetworkEvent grows +// when createNetworkNeighbor() (container_data.go) turns it into a v1beta1.NetworkNeighbor +// at serialization time: a generated Identifier hash (sha256 hex, 64 bytes), a Type string, +// a Ports entry, and - when DNS resolution or a Pod/Service selector applies - a DNS name or +// label-selector map that has no counterpart on the raw event. None of that can be computed +// exactly at report time, since DNS resolution and the Service selector lookup only happen +// at serialization, so this is a fixed conservative surcharge rather than a live measurement. +const networkNeighborExpansionEstimate = 256 + // ReportCapability reports a capability event for a container func (cpm *ContainerProfileManager) ReportCapability(containerID, capability string) { err := cpm.withContainer(containerID, func(data *containerData) (int, error) { @@ -273,7 +282,7 @@ func (cpm *ContainerProfileManager) ReportNetworkEvent(containerID string, event } data.networks.Add(networkEvent) - return size.Of(networkEvent), nil + return size.Of(networkEvent) + networkNeighborExpansionEstimate, nil }) cpm.logEventError(err, "network", containerID) @@ -299,7 +308,13 @@ func (cpm *ContainerProfileManager) ReportSyscall(containerID string, syscall st if data.syscalls == nil { data.syscalls = mapset.NewSet[string]() } - return data.syscalls.Append(syscall), nil + // Append returns the number of elements newly added (0 or 1 here), not their + // serialized size - using it directly mixed element-count units into a + // byte-size accumulator and made syscalls contribute ~0 to MaxTsProfileSize. + if data.syscalls.Append(syscall) == 0 { + return 0, nil + } + return size.Of(syscall), nil }) cpm.logEventError(err, "syscalls", containerID) diff --git a/pkg/containerprofilemanager/v1/event_reporting_test.go b/pkg/containerprofilemanager/v1/event_reporting_test.go index ee38683d53..d548c1b6a3 100644 --- a/pkg/containerprofilemanager/v1/event_reporting_test.go +++ b/pkg/containerprofilemanager/v1/event_reporting_test.go @@ -1,6 +1,75 @@ package containerprofilemanager -import "testing" +import ( + "testing" + + "github.com/DmitriyVTitov/size" + "github.com/inspektor-gadget/inspektor-gadget/pkg/types" + "github.com/kubescape/node-agent/pkg/config" + "github.com/kubescape/node-agent/pkg/utils" + "github.com/stretchr/testify/assert" +) + +// newTestManager builds a ContainerProfileManager with a single, pre-registered +// container entry, large enough MaxTsProfileSize that these tests never trip the +// split path, and no watchedContainerData - so a threshold crossing is a no-op +// instead of blocking on an unbuffered SyncChannel send. +func newTestManager(t *testing.T, containerID string) (*ContainerProfileManager, *ContainerEntry) { + t.Helper() + cpm := &ContainerProfileManager{ + cfg: config.Config{MaxTsProfileSize: 10 * 1024 * 1024}, + containers: map[string]*ContainerEntry{}, + } + entry := &ContainerEntry{data: &containerData{}} + cpm.addContainerEntry(containerID, entry) + return cpm, entry +} + +func TestReportSyscallSizeAccounting(t *testing.T) { + cpm, entry := newTestManager(t, "container1") + + cpm.ReportSyscall("container1", "execve") + assert.Equal(t, int64(size.Of("execve")), entry.data.size.Load(), + "size must grow by the syscall's byte size, not by the set's element-count delta") + + // Re-reporting an already-known syscall is a set-dedup no-op and must not grow the estimate. + cpm.ReportSyscall("container1", "execve") + assert.Equal(t, int64(size.Of("execve")), entry.data.size.Load()) + + cpm.ReportSyscall("container1", "openat") + assert.Equal(t, int64(size.Of("execve")+size.Of("openat")), entry.data.size.Load()) +} + +func TestReportNetworkEventSizeAccounting(t *testing.T) { + cpm, entry := newTestManager(t, "container1") + + event := &utils.StructEvent{ + DstEndpoint: types.L3Endpoint{ + Addr: "10.0.0.5", + }, + DstPort: 8080, + Proto: "tcp", + PktType: utils.OutgoingPktType, + } + + cpm.ReportNetworkEvent("container1", event) + + networkEvent := NetworkEvent{ + Port: 8080, + Protocol: "tcp", + PktType: utils.OutgoingPktType, + Destination: Destination{ + IPAddress: "10.0.0.5", + }, + } + want := int64(size.Of(networkEvent) + networkNeighborExpansionEstimate) + assert.Equal(t, want, entry.data.size.Load(), + "estimate must include the networkNeighborExpansionEstimate surcharge for the DNS/selector/identifier fields createNetworkNeighbor adds at serialization time") + + // Re-reporting the identical event is a set-dedup no-op and must not grow the estimate. + cpm.ReportNetworkEvent("container1", event) + assert.Equal(t, want, entry.data.size.Load()) +} func TestResolveExecPath(t *testing.T) { tests := []struct { From b9ad48cf5e943f277951ce4db3f44f5833265c12 Mon Sep 17 00:00:00 2001 From: aryanghai12 Date: Wed, 5 Aug 2026 13:04:27 +0530 Subject: [PATCH 2/3] fix(containerprofile): derive networkNeighborExpansionEstimate from documented bounds The flat 256-byte surcharge was an unjustified guess: RFC 1035's max DNS name (253 bytes) alone, stored twice in DNS/DNSNames, already exceeds it before counting the Identifier, Ports entry, or selector maps createNetworkNeighbor adds at serialization. Replace it with a value computed from each field's documented worst case (sha256-hex identifier, RFC 1035 DNS name, exact NamespaceSelector shape, a generously budgeted PodSelector label count), and add tests that run the real createNetworkNeighbor path against a max-length DNS name and a populated selector payload to confirm the estimate covers it. Signed-off-by: aryanghai12 --- .../v1/event_reporting.go | 71 +++++++++++++++-- .../v1/event_reporting_test.go | 76 +++++++++++++++++++ 2 files changed, 139 insertions(+), 8 deletions(-) diff --git a/pkg/containerprofilemanager/v1/event_reporting.go b/pkg/containerprofilemanager/v1/event_reporting.go index c8e985b017..57348ae3c2 100644 --- a/pkg/containerprofilemanager/v1/event_reporting.go +++ b/pkg/containerprofilemanager/v1/event_reporting.go @@ -1,6 +1,7 @@ package containerprofilemanager import ( + "crypto/sha256" "errors" "reflect" "regexp" @@ -15,18 +16,72 @@ import ( "github.com/kubescape/node-agent/pkg/utils" "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" "github.com/kubescape/storage/pkg/registry/file/dynamicpathdetector" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" ) var procRegex = regexp.MustCompile(`^/proc/\d+`) -// networkNeighborExpansionEstimate approximates, in bytes, how much a NetworkEvent grows -// when createNetworkNeighbor() (container_data.go) turns it into a v1beta1.NetworkNeighbor -// at serialization time: a generated Identifier hash (sha256 hex, 64 bytes), a Type string, -// a Ports entry, and - when DNS resolution or a Pod/Service selector applies - a DNS name or -// label-selector map that has no counterpart on the raw event. None of that can be computed -// exactly at report time, since DNS resolution and the Service selector lookup only happen -// at serialization, so this is a fixed conservative surcharge rather than a live measurement. -const networkNeighborExpansionEstimate = 256 +// networkNeighborExpansionEstimate is the number of bytes a NetworkEvent gains when +// createNetworkNeighbor() (container_data.go) turns it into a v1beta1.NetworkNeighbor at +// serialization time. None of these fields exist on the raw event, and none can be computed +// exactly at report time since DNS resolution and the Service selector lookup are deferred +// to serialization, so each is sized at its documented worst case instead of guessed: +// +// - Identifier: hex.EncodeToString of a sha256 sum, always exactly 2*sha256.Size bytes. +// - Type: the longer of "internal"/"external". +// - Ports: createNetworkNeighbor always appends exactly one NetworkPort entry. +// - DNS/DNSNames: the longest legal DNS name (RFC 1035 §3.1: 253 bytes), stored once in +// DNS and again in DNSNames - previously undercounted entirely (253*2 alone exceeds the +// old flat 256-byte guess this replaces). +// - NamespaceSelector: getNamespaceMatchLabels always produces exactly one entry keyed +// "kubernetes.io/metadata.name", valued with a namespace name (DNS-1123 label, RFC 1123: +// 63 bytes max) - this bound is exact, not assumed. +// - PodSelector: filterLabels forwards whatever label set the destination pod has. Core +// Kubernetes caps a label's key (253 bytes, optional DNS-subdomain prefix + "/" + 63-byte +// name) and value (63 bytes) but not the number of labels on an object, so no fixed bound +// is exact here. maxBudgetedPodLabels labels at that per-label maximum is budgeted as a +// documented, deliberately generous headroom for typical (e.g. Helm-templated) workloads; +// a pod with more labels than that could still push a profile past MaxTsProfileSize before +// this estimator catches it, in which case the queue-level split from #866 is the backstop. +// +// A single event never produces DNS and both selectors at once (createNetworkNeighbor takes +// one branch per Destination.Kind), so summing every component here is deliberately +// conservative on top of the already-generous per-field bounds. +var networkNeighborExpansionEstimate = computeNetworkNeighborExpansionEstimate() + +const maxBudgetedPodLabels = 12 + +func computeNetworkNeighborExpansionEstimate() int { + identifier := strings.Repeat("f", sha256.Size*2) + maxDNSName := strings.Repeat("a", 253) + maxLabelKey := strings.Repeat("k", 253) + maxLabelValue := strings.Repeat("v", 63) + + ports := []v1beta1.NetworkPort{{ + Name: "protocol-65535", + Protocol: v1beta1.ProtocolTCP, + Port: ptr.To(int32(65535)), + }} + + namespaceSelector := &metav1.LabelSelector{ + MatchLabels: map[string]string{"kubernetes.io/metadata.name": maxLabelValue}, + } + + podLabels := make(map[string]string, maxBudgetedPodLabels) + for i := 0; i < maxBudgetedPodLabels; i++ { + // Trailing rune only exists to keep the map keys distinct; length is still ~maxLabelKey. + podLabels[maxLabelKey+string(rune('a'+i))] = maxLabelValue + } + podSelector := &metav1.LabelSelector{MatchLabels: podLabels} + + return size.Of(identifier) + + size.Of(ExternalTrafficType) + + size.Of(ports) + + size.Of(maxDNSName) + size.Of([]string{maxDNSName}) + + size.Of(namespaceSelector) + + size.Of(podSelector) +} // ReportCapability reports a capability event for a container func (cpm *ContainerProfileManager) ReportCapability(containerID, capability string) { diff --git a/pkg/containerprofilemanager/v1/event_reporting_test.go b/pkg/containerprofilemanager/v1/event_reporting_test.go index d548c1b6a3..99f81c5337 100644 --- a/pkg/containerprofilemanager/v1/event_reporting_test.go +++ b/pkg/containerprofilemanager/v1/event_reporting_test.go @@ -1,15 +1,26 @@ package containerprofilemanager import ( + "strings" "testing" "github.com/DmitriyVTitov/size" + mapset "github.com/deckarep/golang-set/v2" "github.com/inspektor-gadget/inspektor-gadget/pkg/types" "github.com/kubescape/node-agent/pkg/config" "github.com/kubescape/node-agent/pkg/utils" "github.com/stretchr/testify/assert" ) +// fakeDNSResolver resolves every address to a fixed domain, so tests can exercise +// createNetworkNeighbor's DNS branch without a real dnsmanager. +type fakeDNSResolver struct{ domain string } + +func (f fakeDNSResolver) ResolveIPAddress(string) (string, bool) { return f.domain, true } +func (f fakeDNSResolver) ResolveContainerProcessToCloudServices(string, uint32) mapset.Set[string] { + return nil +} + // newTestManager builds a ContainerProfileManager with a single, pre-registered // container entry, large enough MaxTsProfileSize that these tests never trip the // split path, and no watchedContainerData - so a threshold crossing is a no-op @@ -71,6 +82,71 @@ func TestReportNetworkEventSizeAccounting(t *testing.T) { assert.Equal(t, want, entry.data.size.Load()) } +// TestNetworkNeighborExpansionEstimateCoversMaxDNSName confirms the report-time estimate +// does not undercount a NetworkNeighbor carrying the longest legal DNS name (RFC 1035 +// §3.1, 253 bytes) once DNS resolution actually runs at serialization time. +func TestNetworkNeighborExpansionEstimateCoversMaxDNSName(t *testing.T) { + maxDNSName := strings.Repeat("a", 253) + + networkEvent := NetworkEvent{ + Port: 443, + Protocol: "tcp", + PktType: utils.OutgoingPktType, + Destination: Destination{ + Kind: EndpointKindRaw, + IPAddress: "203.0.113.10", + }, + } + + cd := &containerData{} + neighbor := cd.createNetworkNeighbor(networkEvent, "default", nil, fakeDNSResolver{domain: maxDNSName}) + if !assert.NotNil(t, neighbor) { + return + } + + estimate := size.Of(networkEvent) + networkNeighborExpansionEstimate + assert.GreaterOrEqual(t, estimate, size.Of(*neighbor), + "report-time estimate must cover a resolved NetworkNeighbor with the longest legal DNS name") +} + +// TestNetworkNeighborExpansionEstimateCoversSelectorPayload confirms the report-time +// estimate does not undercount a NetworkNeighbor whose PodSelector/NamespaceSelector are +// populated from the destination pod's labels at serialization time. +func TestNetworkNeighborExpansionEstimateCoversSelectorPayload(t *testing.T) { + podLabels := map[string]string{ + "app.kubernetes.io/name": "web", + "app.kubernetes.io/instance": "web-abc123", + "app.kubernetes.io/version": "1.4.2", + "app.kubernetes.io/component": "frontend", + "app.kubernetes.io/part-of": "shop", + "app.kubernetes.io/managed-by": "helm", + } + + networkEvent := NetworkEvent{ + Port: 8080, + Protocol: "tcp", + PktType: utils.OutgoingPktType, + Destination: Destination{ + Kind: EndpointKindPod, + Namespace: "other-ns", + Name: "web", + }, + } + networkEvent.SetDestinationPodLabels(podLabels) + + cd := &containerData{} + // namespace "default" differs from the destination's "other-ns", so both PodSelector and + // NamespaceSelector get populated - matching a real cross-namespace neighbor. + neighbor := cd.createNetworkNeighbor(networkEvent, "default", nil, nil) + if !assert.NotNil(t, neighbor) { + return + } + + estimate := size.Of(networkEvent) + networkNeighborExpansionEstimate + assert.GreaterOrEqual(t, estimate, size.Of(*neighbor), + "report-time estimate must cover a NetworkNeighbor with a populated selector payload") +} + func TestResolveExecPath(t *testing.T) { tests := []struct { name string From fe073b2bf5701793a8f307dfd90c79ca727210d7 Mon Sep 17 00:00:00 2001 From: aryanghai12 Date: Fri, 7 Aug 2026 01:05:49 +0530 Subject: [PATCH 3/3] fix(containerprofile): scope network neighbor size estimate to the actual serialization branch The flat surcharge summed every createNetworkNeighbor branch onto every event, overcounting by 6-20x and making ProfileRequiresSplit the normal path instead of a rare backstop. Charge only the branch Destination.Kind actually takes: Ports/NamespaceSelector are computed exactly from data already on the event, PodSelector charges only the map-wrapping delta over what's already counted via Destination.PodLabels, and DNS/Service-selector budgets apply only on their respective branches. Drop LFX_AGENT_SANDBOX_PREP.md from .gitignore (moved to .git/info/exclude) and restore the file's trailing newline. Signed-off-by: aryanghai12 --- .gitignore | 1 - .../v1/event_reporting.go | 131 ++++++++++-------- .../v1/event_reporting_test.go | 33 +++-- 3 files changed, 95 insertions(+), 70 deletions(-) diff --git a/.gitignore b/.gitignore index d56d707255..d930c89479 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,3 @@ resources/ebpf/falco/* node-agent __pycache__ tracers.tar -LFX_AGENT_SANDBOX_PREP.md \ No newline at end of file diff --git a/pkg/containerprofilemanager/v1/event_reporting.go b/pkg/containerprofilemanager/v1/event_reporting.go index 57348ae3c2..5ade624c4f 100644 --- a/pkg/containerprofilemanager/v1/event_reporting.go +++ b/pkg/containerprofilemanager/v1/event_reporting.go @@ -22,65 +22,88 @@ import ( var procRegex = regexp.MustCompile(`^/proc/\d+`) -// networkNeighborExpansionEstimate is the number of bytes a NetworkEvent gains when -// createNetworkNeighbor() (container_data.go) turns it into a v1beta1.NetworkNeighbor at -// serialization time. None of these fields exist on the raw event, and none can be computed -// exactly at report time since DNS resolution and the Service selector lookup are deferred -// to serialization, so each is sized at its documented worst case instead of guessed: -// -// - Identifier: hex.EncodeToString of a sha256 sum, always exactly 2*sha256.Size bytes. -// - Type: the longer of "internal"/"external". -// - Ports: createNetworkNeighbor always appends exactly one NetworkPort entry. -// - DNS/DNSNames: the longest legal DNS name (RFC 1035 §3.1: 253 bytes), stored once in -// DNS and again in DNSNames - previously undercounted entirely (253*2 alone exceeds the -// old flat 256-byte guess this replaces). -// - NamespaceSelector: getNamespaceMatchLabels always produces exactly one entry keyed -// "kubernetes.io/metadata.name", valued with a namespace name (DNS-1123 label, RFC 1123: -// 63 bytes max) - this bound is exact, not assumed. -// - PodSelector: filterLabels forwards whatever label set the destination pod has. Core -// Kubernetes caps a label's key (253 bytes, optional DNS-subdomain prefix + "/" + 63-byte -// name) and value (63 bytes) but not the number of labels on an object, so no fixed bound -// is exact here. maxBudgetedPodLabels labels at that per-label maximum is budgeted as a -// documented, deliberately generous headroom for typical (e.g. Helm-templated) workloads; -// a pod with more labels than that could still push a profile past MaxTsProfileSize before -// this estimator catches it, in which case the queue-level split from #866 is the backstop. -// -// A single event never produces DNS and both selectors at once (createNetworkNeighbor takes -// one branch per Destination.Kind), so summing every component here is deliberately -// conservative on top of the already-generous per-field bounds. -var networkNeighborExpansionEstimate = computeNetworkNeighborExpansionEstimate() - -const maxBudgetedPodLabels = 12 - -func computeNetworkNeighborExpansionEstimate() int { - identifier := strings.Repeat("f", sha256.Size*2) +// neighborFixedOverhead is the number of bytes every v1beta1.NetworkNeighbor gains over its +// source NetworkEvent regardless of Destination.Kind: a generated Identifier hash and a Type +// string. Both are exactly bounded, so they're measured once rather than guessed: +// Identifier is hex.EncodeToString of a sha256 sum (always 2*sha256.Size bytes), Type is the +// longer of "internal"/"external". +var neighborFixedOverhead = size.Of(strings.Repeat("f", sha256.Size*2)) + size.Of(ExternalTrafficType) + +// maxDNSNameEstimate budgets the one field that's genuinely unknowable at report time on +// container_data.go's raw/DNS branch: DNS resolution happens at serialization, not when the +// event is reported. RFC 1035 §3.1 bounds an encoded domain name to 253 bytes; +// createNetworkNeighbor stores it twice (DNS and DNSNames[0]). +var maxDNSNameEstimate = func() int { maxDNSName := strings.Repeat("a", 253) + return size.Of(maxDNSName) + size.Of([]string{maxDNSName}) +}() + +// maxBudgetedServiceLabels bounds maxServiceSelectorEstimate below. Kubernetes Services are +// conventionally selected on a handful of short labels (e.g. "app: foo"), unlike Pods which +// can carry many more, so this is deliberately smaller than a Pod label budget would be. +const maxBudgetedServiceLabels = 6 + +// maxServiceSelectorEstimate budgets the other field genuinely unknowable at report time: +// on the Service branch, svc.GetServiceSelector() is fetched from the k8s API at +// serialization time and has no relationship to anything on the raw event. Each of +// maxBudgetedServiceLabels labels is sized at Kubernetes' per-label maximum (253-byte key, +// 63-byte value) as generous headroom. +var maxServiceSelectorEstimate = func() int { maxLabelKey := strings.Repeat("k", 253) maxLabelValue := strings.Repeat("v", 63) - - ports := []v1beta1.NetworkPort{{ - Name: "protocol-65535", - Protocol: v1beta1.ProtocolTCP, - Port: ptr.To(int32(65535)), - }} - - namespaceSelector := &metav1.LabelSelector{ - MatchLabels: map[string]string{"kubernetes.io/metadata.name": maxLabelValue}, + labels := make(map[string]string, maxBudgetedServiceLabels) + for i := 0; i < maxBudgetedServiceLabels; i++ { + // Trailing rune only exists to keep the map keys distinct; length is still ~maxLabelKey. + labels[maxLabelKey+string(rune('a'+i))] = maxLabelValue + } + return size.Of(&metav1.LabelSelector{MatchLabels: labels}) +}() + +// networkNeighborIncrement estimates the additional bytes createNetworkNeighbor() +// (container_data.go) adds beyond the raw NetworkEvent when it builds the eventual +// v1beta1.NetworkNeighbor. createNetworkNeighbor takes exactly one branch per +// Destination.Kind, so only that branch's cost is charged - summing every branch +// unconditionally, as an earlier version of this function did, overcounted by 6-20x and +// turned the split path from #866 into the normal case instead of a rare backstop. +// +// PodSelector on the Pod branch is not budgeted as a guess: its label *bytes* are already on +// the meter via Destination.PodLabels, a string field size.Of(networkEvent) counts by the +// caller, but re-shaping that string into map[string]string costs real additional bytes (Go +// map bucket overhead), so this measures that wrapper delta exactly from the same data +// filterLabels/GetDestinationPodLabels would produce, rather than assuming it's zero or +// guessing a label count. Ports and NamespaceSelector are similarly computed exactly from +// fields already on the event (Port/Protocol, and the destination namespace compared against +// the container's own), not estimated, since nothing about their content is deferred to +// serialization. +func networkNeighborIncrement(data *containerData, networkEvent NetworkEvent) int { + est := neighborFixedOverhead + size.Of([]v1beta1.NetworkPort{{ + Name: generatePortIdentifierFromEvent(networkEvent), + Protocol: v1beta1.Protocol(networkEvent.Protocol), + Port: ptr.To(int32(networkEvent.Port)), + }}) + + sourceNamespace := "" + if data.watchedContainerData != nil { + sourceNamespace = data.watchedContainerData.Namespace + } + if namespaceLabels := getNamespaceMatchLabels(networkEvent.Destination.Namespace, sourceNamespace); namespaceLabels != nil { + est += size.Of(&metav1.LabelSelector{MatchLabels: namespaceLabels}) } - podLabels := make(map[string]string, maxBudgetedPodLabels) - for i := 0; i < maxBudgetedPodLabels; i++ { - // Trailing rune only exists to keep the map keys distinct; length is still ~maxLabelKey. - podLabels[maxLabelKey+string(rune('a'+i))] = maxLabelValue + switch networkEvent.Destination.Kind { + case EndpointKindService: + est += maxServiceSelectorEstimate + case EndpointKindPod: + // The label bytes are already on the meter via Destination.PodLabels; only charge the + // extra cost of wrapping them into a LabelSelector's map, and never a negative one. + podSelector := &metav1.LabelSelector{MatchLabels: filterLabels(networkEvent.GetDestinationPodLabels())} + if delta := size.Of(podSelector) - size.Of(networkEvent.Destination.PodLabels); delta > 0 { + est += delta + } + default: + est += maxDNSNameEstimate } - podSelector := &metav1.LabelSelector{MatchLabels: podLabels} - - return size.Of(identifier) + - size.Of(ExternalTrafficType) + - size.Of(ports) + - size.Of(maxDNSName) + size.Of([]string{maxDNSName}) + - size.Of(namespaceSelector) + - size.Of(podSelector) + return est } // ReportCapability reports a capability event for a container @@ -337,7 +360,7 @@ func (cpm *ContainerProfileManager) ReportNetworkEvent(containerID string, event } data.networks.Add(networkEvent) - return size.Of(networkEvent) + networkNeighborExpansionEstimate, nil + return size.Of(networkEvent) + networkNeighborIncrement(data, networkEvent), nil }) cpm.logEventError(err, "network", containerID) diff --git a/pkg/containerprofilemanager/v1/event_reporting_test.go b/pkg/containerprofilemanager/v1/event_reporting_test.go index 99f81c5337..02fe24ca33 100644 --- a/pkg/containerprofilemanager/v1/event_reporting_test.go +++ b/pkg/containerprofilemanager/v1/event_reporting_test.go @@ -8,6 +8,7 @@ import ( mapset "github.com/deckarep/golang-set/v2" "github.com/inspektor-gadget/inspektor-gadget/pkg/types" "github.com/kubescape/node-agent/pkg/config" + "github.com/kubescape/node-agent/pkg/objectcache" "github.com/kubescape/node-agent/pkg/utils" "github.com/stretchr/testify/assert" ) @@ -73,19 +74,19 @@ func TestReportNetworkEventSizeAccounting(t *testing.T) { IPAddress: "10.0.0.5", }, } - want := int64(size.Of(networkEvent) + networkNeighborExpansionEstimate) + want := int64(size.Of(networkEvent) + networkNeighborIncrement(entry.data, networkEvent)) assert.Equal(t, want, entry.data.size.Load(), - "estimate must include the networkNeighborExpansionEstimate surcharge for the DNS/selector/identifier fields createNetworkNeighbor adds at serialization time") + "estimate must include the networkNeighborIncrement surcharge for the identifier/Ports/DNS fields createNetworkNeighbor adds at serialization time") // Re-reporting the identical event is a set-dedup no-op and must not grow the estimate. cpm.ReportNetworkEvent("container1", event) assert.Equal(t, want, entry.data.size.Load()) } -// TestNetworkNeighborExpansionEstimateCoversMaxDNSName confirms the report-time estimate -// does not undercount a NetworkNeighbor carrying the longest legal DNS name (RFC 1035 -// §3.1, 253 bytes) once DNS resolution actually runs at serialization time. -func TestNetworkNeighborExpansionEstimateCoversMaxDNSName(t *testing.T) { +// TestNetworkNeighborIncrementCoversMaxDNSName confirms the report-time estimate does not +// undercount a NetworkNeighbor carrying the longest legal DNS name (RFC 1035 §3.1, 253 +// bytes) once DNS resolution actually runs at serialization time. +func TestNetworkNeighborIncrementCoversMaxDNSName(t *testing.T) { maxDNSName := strings.Repeat("a", 253) networkEvent := NetworkEvent{ @@ -104,15 +105,15 @@ func TestNetworkNeighborExpansionEstimateCoversMaxDNSName(t *testing.T) { return } - estimate := size.Of(networkEvent) + networkNeighborExpansionEstimate + estimate := size.Of(networkEvent) + networkNeighborIncrement(cd, networkEvent) assert.GreaterOrEqual(t, estimate, size.Of(*neighbor), "report-time estimate must cover a resolved NetworkNeighbor with the longest legal DNS name") } -// TestNetworkNeighborExpansionEstimateCoversSelectorPayload confirms the report-time -// estimate does not undercount a NetworkNeighbor whose PodSelector/NamespaceSelector are -// populated from the destination pod's labels at serialization time. -func TestNetworkNeighborExpansionEstimateCoversSelectorPayload(t *testing.T) { +// TestNetworkNeighborIncrementCoversSelectorPayload confirms the report-time estimate does +// not undercount a NetworkNeighbor whose PodSelector/NamespaceSelector are populated from +// the destination pod's labels at serialization time. +func TestNetworkNeighborIncrementCoversSelectorPayload(t *testing.T) { podLabels := map[string]string{ "app.kubernetes.io/name": "web", "app.kubernetes.io/instance": "web-abc123", @@ -134,15 +135,17 @@ func TestNetworkNeighborExpansionEstimateCoversSelectorPayload(t *testing.T) { } networkEvent.SetDestinationPodLabels(podLabels) - cd := &containerData{} - // namespace "default" differs from the destination's "other-ns", so both PodSelector and - // NamespaceSelector get populated - matching a real cross-namespace neighbor. + // The container's own namespace ("default") differs from the destination's ("other-ns"), + // so both PodSelector and NamespaceSelector get populated - matching a real cross-namespace + // neighbor. watchedContainerData.Namespace is what networkNeighborIncrement reads to make + // the same "different namespace" call createNetworkNeighbor's own namespace arg does below. + cd := &containerData{watchedContainerData: &objectcache.WatchedContainerData{Namespace: "default"}} neighbor := cd.createNetworkNeighbor(networkEvent, "default", nil, nil) if !assert.NotNil(t, neighbor) { return } - estimate := size.Of(networkEvent) + networkNeighborExpansionEstimate + estimate := size.Of(networkEvent) + networkNeighborIncrement(cd, networkEvent) assert.GreaterOrEqual(t, estimate, size.Of(*neighbor), "report-time estimate must cover a NetworkNeighbor with a populated selector payload") }