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
28 changes: 28 additions & 0 deletions pkg/objectcache/containerprofilecache/projection_apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ func Apply(spec *objectcache.RuleProjectionSpec, cp *v1beta1.ContainerProfile, c

execsPaths := extractExecsPaths(cp)
pcp.Execs = projectField(s.Execs, execsPaths, true)
pcp.ExecsByPath = extractExecsByPath(cp)

endpointPaths := extractEndpointPaths(cp)
pcp.Endpoints = projectField(s.Endpoints, endpointPaths, true)
Expand Down Expand Up @@ -166,6 +167,33 @@ func extractExecsPaths(cp *v1beta1.ContainerProfile) []string {
return paths
}

// extractExecsByPath builds the path → args map used by exec-args
// matchers (e.g. dynamicpathdetector.CompareExecArgs in node-agent#807).
// Multiple ExecCalls entries with the same Path collapse to the last
// seen. nil-Args entries are stored as empty slices; downstream
// matchers distinguish "absent key" (path not in the profile at all)
// from "present with empty slice" (path captured but ran with no args).
//
// Args slices are CLONED rather than aliased — Apply is contract-bound
// to be a pure transform, and an alias would let consumers mutate the
// source profile by editing the projected map.
func extractExecsByPath(cp *v1beta1.ContainerProfile) map[string][]string {
if len(cp.Spec.Execs) == 0 {
return nil
}
m := make(map[string][]string, len(cp.Spec.Execs))
for _, e := range cp.Spec.Execs {
if e.Args == nil {
m[e.Path] = []string{}
continue
}
cloned := make([]string, len(e.Args))
copy(cloned, e.Args)
m[e.Path] = cloned
}
return m
}

func extractEndpointPaths(cp *v1beta1.ContainerProfile) []string {
endpoints := make([]string, len(cp.Spec.Endpoints))
for i, e := range cp.Spec.Endpoints {
Expand Down
50 changes: 50 additions & 0 deletions pkg/objectcache/containerprofilecache/projection_apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -410,3 +410,53 @@ func TestApply_ExactFilter_NoMatchYieldsNilValues(t *testing.T) {
require.NotNil(t, pcp)
assert.Nil(t, pcp.Opens.Values, "Values should be nil when no entries match the filter")
}

// TestApply_ExecsByPath_PopulatesFromSpec pins the projection of
// per-Path Args from cp.Spec.Execs into ProjectedContainerProfile.ExecsByPath.
// Three cases combined in one CP fixture to keep the gate compact:
// - Path with a populated Args slice — projected as a CLONED slice
// - Path with nil Args — projected as an empty (non-nil) slice
// - Two ExecCalls with the same Path — last write wins
// The cloned-slice invariant is checked by mutating the projected slice
// and asserting the source is unchanged.
func TestApply_ExecsByPath_PopulatesFromSpec(t *testing.T) {
cp := &v1beta1.ContainerProfile{
Spec: v1beta1.ContainerProfileSpec{
Execs: []v1beta1.ExecCalls{
{Path: "/bin/sh", Args: []string{"-c", "echo hi"}},
{Path: "/bin/echo", Args: nil},
{Path: "/bin/sh", Args: []string{"-x", "later"}},
},
},
}
pcp := Apply(&objectcache.RuleProjectionSpec{}, cp, nil)
require.NotNil(t, pcp)
require.NotNil(t, pcp.ExecsByPath, "ExecsByPath must be populated")

// last write wins for duplicate Path
assert.Equal(t, []string{"-x", "later"}, pcp.ExecsByPath["/bin/sh"],
"duplicate Path: last ExecCalls entry should win")

// nil Args → empty (non-nil) slice
got, present := pcp.ExecsByPath["/bin/echo"]
require.True(t, present, "/bin/echo must be present even with nil Args")
require.NotNil(t, got, "/bin/echo Args nil source must project as non-nil empty slice")
assert.Empty(t, got, "/bin/echo nil-Args must project as empty slice")

// CLONED-slice invariant: mutating the projection must not affect
// the source ContainerProfile spec.
sourceCopy := append([]string{}, cp.Spec.Execs[2].Args...) // current "/bin/sh"
pcp.ExecsByPath["/bin/sh"][0] = "MUTATED"
assert.Equal(t, sourceCopy, cp.Spec.Execs[2].Args,
"mutating the projected slice must not propagate to the source profile (cloned, not aliased)")
}

// TestApply_ExecsByPath_NilWhenSpecEmpty pins the contract that an
// empty Execs list yields a nil ExecsByPath (not an allocated empty
// map). Matches the project-wide convention of nil-for-empty.
func TestApply_ExecsByPath_NilWhenSpecEmpty(t *testing.T) {
cp := &v1beta1.ContainerProfile{Spec: v1beta1.ContainerProfileSpec{}}
pcp := Apply(&objectcache.RuleProjectionSpec{}, cp, nil)
require.NotNil(t, pcp)
assert.Nil(t, pcp.ExecsByPath, "empty Spec.Execs must project to nil ExecsByPath")
}
9 changes: 9 additions & 0 deletions pkg/objectcache/projection_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@ type ProjectedContainerProfile struct {
IngressDomains ProjectedField
IngressAddresses ProjectedField

// ExecsByPath carries the per-Path Args slice from cp.Spec.Execs so
// downstream consumers (e.g. dynamicpathdetector.CompareExecArgs used
// by R0040 in node-agent#807) can run wildcard-aware argv matching
// against the projected profile. Keyed by Exec.Path (same key used
// in Execs.Values / Execs.Patterns). Projection-v1 dropped argv
// matching as "future work"; this field re-adds the storage surface
// without re-introducing the matcher itself.
ExecsByPath map[string][]string

SpecHash string
SyncChecksum string
PolicyByRuleId map[string]v1beta1.RulePolicy
Expand Down