Skip to content

Commit 8ca90c7

Browse files
Merge pull request #294 from kyledong-suse/learn-for-namespace
feat: implement learn configuration
2 parents 9d3e623 + b963ac0 commit 8ca90c7

11 files changed

Lines changed: 547 additions & 219 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,6 @@ internal/bpf/bpf_*bpfeb.o
3939
internal/bpf/bpf_*bpfel.o
4040
internal/bpf/bpf_*bpfeb.go
4141
internal/bpf/bpf_*bpfel.go
42+
43+
# Known API violations reports (generated by codegen)
44+
sample_apiserver_violation_exceptions.list

charts/runtime-enforcer/questions.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,15 @@ questions:
5252
Process enforcer remains unaffected.
5353
group: Learning
5454

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
63+
5564
###############################################################################
5665
# OpenTelemetry
5766
###############################################################################

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ spec:
3535
{{- if .Values.learning.enabled }}
3636
- --enable-learning
3737
{{- end }}
38+
{{- if .Values.learning.namespaceSelector }}
39+
{{- if kindIs "string" .Values.learning.namespaceSelector }}
40+
- --learning-namespace-selector={{ .Values.learning.namespaceSelector }}
41+
{{- else }}
42+
- --learning-namespace-selector={{ .Values.learning.namespaceSelector | toJson }}
43+
{{- end }}
44+
{{- end }}
3845
- --grpc-port={{ .Values.agent.agent.grpcExporterPort }}
3946
- --grpc-mtls-cert-dir={{ include "runtime-enforcer.grpc.certDir" . }}
4047
- --log-level={{ .Values.agent.agent.logLevel }}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ kind: ClusterRole
44
metadata:
55
name: {{ include "runtime-enforcer.fullname" . }}-agent
66
rules:
7+
- apiGroups:
8+
- ""
9+
resources:
10+
- namespaces
11+
verbs:
12+
- get
13+
- list
14+
- watch
715
- apiGroups:
816
- security.rancher.io
917
resources:

charts/runtime-enforcer/values.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,18 @@ learning:
100100
# learning.enabled controls whether learning mode is enabled for the entire cluster.
101101
# When disabled, no process learning occurs. Process enforcer remains unaffected.
102102
enabled: true
103+
# Label selector for namespaces to include in learning (empty = all).
104+
# You can set either a string or a YAML/JSON object.
105+
#
106+
# String format:
107+
# namespaceSelector: "" # learn from all namespaces
108+
# namespaceSelector: "env=prod" # namespaces with label env=prod
109+
#
110+
# YAML/JSON object format:
111+
# namespaceSelector:
112+
# matchLabels:
113+
# env: prod
114+
namespaceSelector: ""
103115

104116
telemetry:
105117
# runtime-enforcer telemetry configuration allow one OpenTelemetry

cmd/agent/main.go

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"context"
5+
"encoding/json"
56
"errors"
67
"flag"
78
"fmt"
@@ -16,6 +17,8 @@ import (
1617
"github.com/rancher-sandbox/runtime-enforcer/internal/grpcexporter"
1718
"github.com/rancher-sandbox/runtime-enforcer/internal/nri"
1819
"github.com/rancher-sandbox/runtime-enforcer/internal/resolver"
20+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
21+
"k8s.io/apimachinery/pkg/labels"
1922
"k8s.io/apimachinery/pkg/runtime"
2023
ctrl "sigs.k8s.io/controller-runtime"
2124
"sigs.k8s.io/controller-runtime/pkg/manager"
@@ -30,14 +33,15 @@ import (
3033
)
3134

3235
type Config struct {
33-
enableTracing bool
34-
enableOtelSidecar bool
35-
enableLearning bool
36-
nriSocketPath string
37-
nriPluginIdx string
38-
probeAddr string
39-
grpcConf grpcexporter.Config
40-
logLevel string
36+
enableTracing bool
37+
enableOtelSidecar bool
38+
enableLearning bool
39+
learningNamespaceSelector string
40+
nriSocketPath string
41+
nriPluginIdx string
42+
probeAddr string
43+
grpcConf grpcexporter.Config
44+
logLevel string
4145
}
4246

4347
// +kubebuilder:rbac:groups=security.rancher.io,resources=workloadpolicies,verbs=get;list;watch
@@ -87,11 +91,22 @@ func setupLearningReconciler(
8791
}, nil
8892
}
8993

90-
learningReconciler := eventhandler.NewLearningReconciler(ctrlMgr.GetClient())
94+
var nsSelector labels.Selector
95+
// If the learning namespace selector is empty, the learning will apply to all namespaces.
96+
// Otherwise, we parse the learning namespace selector.
97+
if config.learningNamespaceSelector != "" {
98+
selector, err := parseLearningNamespaceSelector(config.learningNamespaceSelector)
99+
if err != nil {
100+
return nil, fmt.Errorf("invalid learning-namespace-selector %q: %w", config.learningNamespaceSelector, err)
101+
}
102+
nsSelector = selector
103+
}
104+
105+
learningReconciler := eventhandler.NewLearningReconciler(ctrlMgr.GetClient(), nsSelector)
91106
if err := learningReconciler.SetupWithManager(ctrlMgr); err != nil {
92107
return nil, fmt.Errorf("unable to create learning reconciler: %w", err)
93108
}
94-
logger.InfoContext(ctx, "learning mode is enabled")
109+
logger.InfoContext(ctx, "learning mode is enabled", "namespaceSelector", config.learningNamespaceSelector)
95110
return learningReconciler.EnqueueEvent, nil
96111
}
97112

@@ -242,6 +257,21 @@ func parseLogLevel(level string) slog.Level {
242257
}
243258
}
244259

260+
// parseLearningNamespaceSelector parses the learning namespace selector from either:
261+
// - A JSON object (e.g. {"matchLabels":{"env":"prod"}}.
262+
// - A string in Kubernetes label selector format (e.g. "env=prod").
263+
func parseLearningNamespaceSelector(s string) (labels.Selector, error) {
264+
s = strings.TrimSpace(s)
265+
if strings.HasPrefix(s, "{") {
266+
var ls metav1.LabelSelector
267+
if err := json.Unmarshal([]byte(s), &ls); err != nil {
268+
return nil, fmt.Errorf("invalid JSON label selector %q: %w", s, err)
269+
}
270+
return metav1.LabelSelectorAsSelector(&ls)
271+
}
272+
return labels.Parse(s)
273+
}
274+
245275
func main() {
246276
var err error
247277
var config Config
@@ -253,6 +283,12 @@ func main() {
253283
flag.BoolVar(&config.enableTracing, "enable-tracing", false, "Enable tracing collection")
254284
flag.BoolVar(&config.enableOtelSidecar, "enable-otel-sidecar", false, "Enable OpenTelemetry sidecar")
255285
flag.BoolVar(&config.enableLearning, "enable-learning", false, "Enable learning mode")
286+
flag.StringVar(
287+
&config.learningNamespaceSelector,
288+
"learning-namespace-selector",
289+
"",
290+
"Label selector for namespaces to include in learning (empty = all)",
291+
)
256292
flag.StringVar(&config.nriSocketPath, "nri-socket-path", "/var/run/nri/nri.sock", "NRI socket path")
257293
flag.StringVar(&config.nriPluginIdx, "nri-plugin-index", "00", "NRI plugin index")
258294
flag.StringVar(&config.probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")

internal/eventhandler/learning_controller.go

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,12 @@ import (
1010
"go.opentelemetry.io/otel"
1111
"go.opentelemetry.io/otel/attribute"
1212
"go.opentelemetry.io/otel/trace"
13+
corev1 "k8s.io/api/core/v1"
14+
apierrors "k8s.io/apimachinery/pkg/api/errors"
1315
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
16+
"k8s.io/apimachinery/pkg/labels"
1417
"k8s.io/apimachinery/pkg/runtime"
18+
"k8s.io/apimachinery/pkg/types"
1519
"k8s.io/apimachinery/pkg/util/validation"
1620
"k8s.io/client-go/util/workqueue"
1721
ctrl "sigs.k8s.io/controller-runtime"
@@ -62,18 +66,28 @@ func GetWorkloadPolicyProposalName(kind string, resourceName string) (string, er
6266
type LearningReconciler struct {
6367
client.Client
6468

65-
Scheme *runtime.Scheme
66-
eventChan chan event.TypedGenericEvent[eventscraper.KubeProcessInfo]
67-
tracer trace.Tracer
69+
Scheme *runtime.Scheme
70+
eventChan chan event.TypedGenericEvent[eventscraper.KubeProcessInfo]
71+
tracer trace.Tracer
72+
namespaceSelector labels.Selector
6873
// OwnerRefEnricher can be overridden during testing
6974
OwnerRefEnricher func(wp *securityv1alpha1.WorkloadPolicyProposal, workloadKind string, workload string)
7075
}
7176

72-
func NewLearningReconciler(client client.Client) *LearningReconciler {
77+
func NewLearningReconciler(
78+
client client.Client,
79+
selector labels.Selector,
80+
) *LearningReconciler {
7381
return &LearningReconciler{
74-
Client: client,
75-
eventChan: make(chan event.TypedGenericEvent[eventscraper.KubeProcessInfo], DefaultEventChannelBufferSize),
76-
tracer: otel.Tracer("runtime-enforcer-learner"),
82+
Client: client,
83+
eventChan: make(
84+
chan event.TypedGenericEvent[eventscraper.KubeProcessInfo],
85+
DefaultEventChannelBufferSize,
86+
),
87+
tracer: otel.Tracer(
88+
"runtime-enforcer-learner",
89+
),
90+
namespaceSelector: selector,
7791
OwnerRefEnricher: func(wp *securityv1alpha1.WorkloadPolicyProposal, workloadKind string, workload string) {
7892
wp.OwnerReferences = []metav1.OwnerReference{
7993
{
@@ -85,7 +99,8 @@ func NewLearningReconciler(client client.Client) *LearningReconciler {
8599
}
86100
}
87101

88-
// kubebuilder annotations for accessing policy proposals.
102+
// kubebuilder annotations for accessing policy proposals and namespaces.
103+
// +kubebuilder:rbac:groups="",resources=namespaces,verbs=get;list;watch
89104
// +kubebuilder:rbac:groups=security.rancher.io,resources=workloadpolicyproposals,verbs=create;get;list;watch;update;patch
90105

91106
// Reconcile receives learning events and creates/updates WorkloadPolicyProposal resources accordingly.
@@ -113,6 +128,25 @@ func (r *LearningReconciler) Reconcile(
113128
return ctrl.Result{}, nil
114129
}
115130

131+
if r.namespaceSelector != nil {
132+
var ns corev1.Namespace
133+
if err = r.Client.Get(ctx, types.NamespacedName{Name: req.Namespace}, &ns); err != nil {
134+
if apierrors.IsNotFound(err) {
135+
log.V(3).Info( //nolint:mnd // 3 is the verbosity level for detailed debug info
136+
"Namespace not found while evaluating learning namespace selector",
137+
"namespace", req.Namespace,
138+
)
139+
return ctrl.Result{}, nil
140+
}
141+
return ctrl.Result{}, fmt.Errorf("failed to get namespace %s: %w", req.Namespace, err)
142+
}
143+
if !r.namespaceSelector.Matches(labels.Set(ns.GetLabels())) {
144+
log.V(1).
145+
Info("Namespace does not match learning namespace selector; skipping event", "namespace", req.Namespace)
146+
return ctrl.Result{}, nil
147+
}
148+
}
149+
116150
proposalName, err = GetWorkloadPolicyProposalName(req.WorkloadKind, req.Workload)
117151
if err != nil {
118152
return ctrl.Result{}, fmt.Errorf("failed to get proposal name: %w", err)

0 commit comments

Comments
 (0)