Skip to content

Commit 359c14f

Browse files
authored
Merge pull request #476 from Andreagit97/improve-namespace-selector
feat: rework `learning` config
2 parents 5ac42f7 + f342ed3 commit 359c14f

15 files changed

Lines changed: 457 additions & 545 deletions

charts/runtime-enforcer/questions.yaml

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -43,23 +43,10 @@ questions:
4343
###############################################################################
4444
# Learning
4545
###############################################################################
46-
- variable: learning.enabled
47-
type: boolean
48-
default: true
49-
label: Enable Cluster-wide Learning Mode
50-
description: |
51-
Enable cluster-wide learning mode. If disabled, no process learning occurs.
52-
Process enforcer remains unaffected.
53-
group: Learning
5446

55-
- variable: learning.namespaceSelector
56-
type: string
57-
default: ""
58-
label: Namespace Selector
59-
description: |
60-
Label selector for namespaces to include in learning (empty = all).
61-
group: Learning
62-
show_if: learning.enabled=true
47+
# learning.namespaceSelector is a complex object (Kubernetes LabelSelector) that
48+
# cannot be represented as a simple Rancher UI field. Configure it directly in
49+
# values.yaml. Set it to {} to disable learning for all namespaces.
6350

6451
###############################################################################
6552
# Telemetry
@@ -106,4 +93,3 @@ questions:
10693
The secret should contain the keys `tls.crt` and `tls.key`.
10794
group: Telemetry
10895
show_if: telemetry.collectorStrategy=external
109-

charts/runtime-enforcer/templates/agent/daemonset.yaml

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,8 @@ spec:
3535
{{- end }}
3636
containers:
3737
- args:
38-
{{- if .Values.learning.enabled }}
39-
- --enable-learning
40-
{{- end }}
4138
{{- if .Values.learning.namespaceSelector }}
42-
- --learning-namespace-selector={{ .Values.learning.namespaceSelector }}
43-
{{- else if .Values.learning.namespaceSelectorObject }}
44-
- --learning-namespace-selector={{ .Values.learning.namespaceSelectorObject | toJson }}
39+
- --learning-namespace-selector={{ .Values.learning.namespaceSelector | toJson }}
4540
{{- end }}
4641
- --grpc-port={{ .Values.agent.grpcExporterPort }}
4742
- --grpc-mtls-cert-dir={{ include "runtime-enforcer.grpc.certDir" . }}

charts/runtime-enforcer/tests/daemonset_test.yaml

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,23 +21,38 @@ tests:
2121
path: "spec.template.spec.priorityClassName"
2222
value: "runtime-enforcer-priorityclass"
2323

24-
- it: "should include --enable-learning flag when learning is enabled"
24+
- it: "should not include learning flag when there is no selector"
2525
set:
2626
learning:
27-
enabled: true
27+
# this `null` overwrites the default in our values.yaml while with {}
28+
# we will have a merge
29+
namespaceSelector: null
30+
asserts:
31+
- notContains:
32+
path: "spec.template.spec.containers[0].args"
33+
content: '--learning-namespace-selector={"matchExpressions":[{"key":"kubernetes.io/metadata.name","operator":"Exists"}]}'
34+
35+
- it: "should include default selector"
36+
set:
37+
learning:
38+
# with {} we have a merge with the default in our values.yaml
39+
namespaceSelector: {}
2840
asserts:
2941
- contains:
3042
path: "spec.template.spec.containers[0].args"
31-
content: "--enable-learning"
43+
content: '--learning-namespace-selector={"matchExpressions":[{"key":"kubernetes.io/metadata.name","operator":"Exists"}]}'
3244

33-
- it: "should not include --enable-learning flag when learning is disabled"
45+
- it: "should render a custom learning namespace selector"
3446
set:
3547
learning:
36-
enabled: false
48+
namespaceSelector:
49+
matchExpressions: null
50+
matchLabels:
51+
env: prod
3752
asserts:
38-
- notContains:
53+
- contains:
3954
path: "spec.template.spec.containers[0].args"
40-
content: "--enable-learning"
55+
content: '--learning-namespace-selector={"matchLabels":{"env":"prod"}}'
4156

4257
- it: "should include grpc port argument"
4358
set:

charts/runtime-enforcer/values.schema.json

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -412,14 +412,25 @@
412412
"learning": {
413413
"type": "object",
414414
"properties": {
415-
"enabled": {
416-
"type": "boolean"
417-
},
418415
"namespaceSelector": {
419-
"type": "string"
420-
},
421-
"namespaceSelectorObject": {
422416
"type": "object",
417+
"properties": {
418+
"matchExpressions": {
419+
"type": "array",
420+
"items": {
421+
"type": "object",
422+
"properties": {
423+
"key": {
424+
"type": "string"
425+
},
426+
"operator": {
427+
"type": "string"
428+
}
429+
},
430+
"additionalProperties": false
431+
}
432+
}
433+
},
423434
"additionalProperties": true
424435
}
425436
},

charts/runtime-enforcer/values.yaml

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -161,20 +161,14 @@ debugger:
161161

162162
# Learning mode configuration
163163
learning:
164-
# learning.enabled controls whether learning mode is enabled for the entire cluster.
165-
# When disabled, no process learning occurs. Process enforcer remains unaffected.
166-
enabled: true
167-
# Label selector for namespaces to include in learning.
168-
# namespaceSelector: "" # learn from all namespaces
169-
# namespaceSelector: "env=prod" # namespaces with label env=prod
170-
namespaceSelector: ""
171-
# Label selector for namespaces to include in learning.
172-
# namespaceSelectorObject: {} # learn from all namespaces
173-
# namespaceSelectorObject: # namespaces with label env=prod
174-
# matchLabels:
175-
# env: prod
176-
# @schema additionalProperties:true
177-
namespaceSelectorObject: {}
164+
# The default value enables learning inside all namespaces of the cluster.
165+
# Override namespaceSelector to limit the learning scope.
166+
# Set namespaceSelector to {} to disable learning for all namespaces.
167+
# @schema additionalProperties:true
168+
namespaceSelector:
169+
matchExpressions:
170+
- key: kubernetes.io/metadata.name
171+
operator: Exists
178172

179173
telemetry:
180174
collectorStrategy: "default" # @schema enum: [none, default, external]

cmd/agent/main.go

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ import (
4040
)
4141

4242
type Config struct {
43-
enableLearning bool
4443
learningNamespaceSelector string
4544
nriSocketPath string
4645
nriPluginIdx string
@@ -56,6 +55,10 @@ type Config struct {
5655
violationLogger otellog.Logger
5756
}
5857

58+
func (c Config) learningEnabled() bool {
59+
return strings.TrimSpace(c.learningNamespaceSelector) != ""
60+
}
61+
5962
func newControllerManager(config Config) (manager.Manager, error) {
6063
scheme := runtime.NewScheme()
6164
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
@@ -165,31 +168,27 @@ func setupLearningReconciler(
165168
config Config,
166169
ctrlMgr manager.Manager,
167170
) (func(eventscraper.KubeProcessInfo), error) {
168-
if !config.enableLearning {
171+
if !config.learningEnabled() {
169172
logger.InfoContext(ctx, "learning mode is disabled")
170173
return func(_ eventscraper.KubeProcessInfo) {
171174
panic("enqueue function should be never called when learning is disabled")
172175
}, nil
173176
}
174177

175178
var nsSelector labels.Selector
176-
// If the learning namespace selector is empty, the learning will apply to all namespaces.
177-
// Otherwise, we parse the learning namespace selector.
178-
if config.learningNamespaceSelector != "" {
179-
selector, err := parseLearningNamespaceSelector(config.learningNamespaceSelector)
180-
if err != nil {
181-
return nil, fmt.Errorf("invalid learning-namespace-selector %q: %w", config.learningNamespaceSelector, err)
182-
}
183-
nsSelector = selector
179+
selector, err := parseLearningNamespaceSelector(config.learningNamespaceSelector)
180+
if err != nil {
181+
return nil, fmt.Errorf("invalid learning-namespace-selector %q: %w", config.learningNamespaceSelector, err)
184182
}
183+
nsSelector = selector
185184

186185
// Wait until mutating admission webhook is ready.
187-
if err := waitForMutatingAdmissionWebhook(ctx); err != nil {
186+
if err = waitForMutatingAdmissionWebhook(ctx); err != nil {
188187
return nil, err
189188
}
190189

191190
learningReconciler := eventhandler.NewLearningReconciler(ctrlMgr.GetClient(), nsSelector)
192-
if err := learningReconciler.SetupWithManager(ctrlMgr); err != nil {
191+
if err = learningReconciler.SetupWithManager(ctrlMgr); err != nil {
193192
return nil, fmt.Errorf("unable to create learning reconciler: %w", err)
194193
}
195194
logger.InfoContext(ctx, "learning mode is enabled", "namespaceSelector", config.learningNamespaceSelector)
@@ -210,7 +209,7 @@ func startAgent(ctx context.Context, logger *slog.Logger, config Config) error {
210209
//////////////////////
211210
// Create BPF manager
212211
//////////////////////
213-
bpfManager, err := bpf.NewManager(logger, config.enableLearning)
212+
bpfManager, err := bpf.NewManager(logger, config.learningEnabled())
214213
if err != nil {
215214
return fmt.Errorf("cannot create BPF manager: %w", err)
216215
}
@@ -320,29 +319,36 @@ func parseLogLevel(level string) slog.Level {
320319
}
321320
}
322321

323-
// parseLearningNamespaceSelector parses the learning namespace selector from either:
324-
// - A JSON object (e.g. {"matchLabels":{"env":"prod"}}.
325-
// - A string in Kubernetes label selector format (e.g. "env=prod").
322+
// parseLearningNamespaceSelector parses the learning namespace selector from a JSON object (e.g. {"matchLabels":{"env":"prod"}}).
326323
func parseLearningNamespaceSelector(s string) (labels.Selector, error) {
327324
s = strings.TrimSpace(s)
328-
if strings.HasPrefix(s, "{") {
329-
var ls metav1.LabelSelector
330-
if err := json.Unmarshal([]byte(s), &ls); err != nil {
331-
return nil, fmt.Errorf("invalid JSON label selector %q: %w", s, err)
332-
}
333-
return metav1.LabelSelectorAsSelector(&ls)
325+
if !strings.HasPrefix(s, "{") {
326+
return nil, fmt.Errorf("invalid JSON label selector %q: must be a JSON object", s)
327+
}
328+
329+
var ls metav1.LabelSelector
330+
if err := json.Unmarshal([]byte(s), &ls); err != nil {
331+
return nil, fmt.Errorf("invalid JSON label selector %q: %w", s, err)
332+
}
333+
selector, err := metav1.LabelSelectorAsSelector(&ls)
334+
if err != nil {
335+
return nil, err
336+
}
337+
338+
if selector.Empty() {
339+
return nil, fmt.Errorf("invalid JSON label selector %q: must not be empty if learning is enabled", s)
334340
}
335-
return labels.Parse(s)
341+
return selector, nil
336342
}
337343

338344
func parseFlags() Config {
339345
var config Config
340-
flag.BoolVar(&config.enableLearning, "enable-learning", false, "Enable learning mode")
346+
// If we receive something different from "", it should be a valid json
341347
flag.StringVar(
342348
&config.learningNamespaceSelector,
343349
"learning-namespace-selector",
344350
"",
345-
"Label selector for namespaces to include in learning (empty = all)",
351+
"Namespace selector for learning. Accepts a JSON LabelSelector",
346352
)
347353
flag.StringVar(&config.nriSocketPath, "nri-socket-path", "/var/run/nri/nri.sock", "NRI socket path")
348354
flag.StringVar(&config.nriPluginIdx, "nri-plugin-index", "00", "NRI plugin index")

docs/learning_mode_configuration.adoc

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ By default, Runtime-Enforcer starts in *learning* mode for **all namespaces**:
99

1010
```yaml
1111
learning:
12-
enabled: true
13-
namespaceSelector: ""
12+
namespaceSelector:
13+
matchExpressions:
14+
- key: kubernetes.io/metadata.name
15+
operator: Exists
1416
```
1517

1618
You can adjust this behaviour at install/upgrade time:
@@ -20,16 +22,13 @@ You can adjust this behaviour at install/upgrade time:
2022
```bash
2123
helm upgrade --install runtime-enforcer runtime-enforcer/runtime-enforcer \
2224
--namespace runtime-enforcer \
23-
--set learning.enabled=false
25+
--set-json 'learning.namespaceSelector={}'
2426
```
2527

26-
- Restrict learning to specific namespaces using a label selector:
28+
- Restrict learning to specific namespaces using a namespaceSelector:
2729
+
2830
```bash
2931
helm upgrade --install runtime-enforcer runtime-enforcer/runtime-enforcer \
3032
--namespace runtime-enforcer \
31-
--set learning.namespaceSelector="env=prod"
33+
--set-json 'learning.namespaceSelector={"matchLabels":{"env":"prod"}}'
3234
```
33-
34-
When `learning.enabled=true`, Runtime-Enforcer will create or update `WorkloadPolicyProposal`
35-
objects only for workloads in namespaces matching `learning.namespaceSelector`.

docs/phases.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Operationally, if you want complete proposals, enable learning first and then *(
2323
=== How to enter and leave the `Learn` phase
2424

2525
* *Enter*
26-
** Ensure learning is enabled on inside of the helm chart values: `values.learning.enabled: true`
26+
** Ensure learning is enabled on some namespaces: `values.learning.namespaceSelector`
2727
** No per-workload configuration is required to start generating proposals (proposals are created as exec events are observed).
2828
* *Leave*
2929
** Mark the corresponding proposal as ready by setting the label: `security.rancher.io/policy-ready=true`

internal/eventhandler/learning_controller.go

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -248,22 +248,18 @@ func (r *LearningReconciler) reconcile(
248248
return ctrl.Result{}, nil
249249
}
250250

251-
if r.namespaceSelector != nil {
252-
var ns corev1.Namespace
253-
if err = r.Client.Get(ctx, types.NamespacedName{Name: req.Namespace}, &ns); err != nil {
254-
if apierrors.IsNotFound(err) {
255-
logger.V(loglevel.VerbosityDebug).Info(
256-
"Namespace not found while evaluating learning namespace selector",
257-
)
258-
return ctrl.Result{}, nil
259-
}
260-
return ctrl.Result{}, fmt.Errorf("failed to get namespace %s: %w", req.Namespace, err)
261-
}
262-
if !r.namespaceSelector.Matches(labels.Set(ns.GetLabels())) {
263-
logger.V(loglevel.VerbosityDebug).
264-
Info("Namespace does not match learning namespace selector; skipping event", "namespace", req.Namespace)
251+
var ns corev1.Namespace
252+
if err = r.Client.Get(ctx, types.NamespacedName{Name: req.Namespace}, &ns); err != nil {
253+
if apierrors.IsNotFound(err) {
254+
logger.V(loglevel.VerbosityDebug).Info(
255+
"Namespace not found while evaluating learning namespace selector",
256+
)
265257
return ctrl.Result{}, nil
266258
}
259+
return ctrl.Result{}, fmt.Errorf("failed to get namespace %s: %w", req.Namespace, err)
260+
}
261+
if !r.namespaceSelector.Matches(labels.Set(ns.GetLabels())) {
262+
return ctrl.Result{}, nil
267263
}
268264

269265
proposalName, err = proposalutils.GetWorkloadPolicyProposalName(req.WorkloadKind, req.Workload)

0 commit comments

Comments
 (0)