Skip to content

Commit 034c590

Browse files
qinqonclaude
andcommitted
feat(handler): auto-detect NetworkManager and use nmstatectl kernel mode
When NetworkManager is not available (e.g., kind containers, minimal hosts), the handler now auto-detects this at startup and transparently switches to nmstatectl kernel mode (-k flag). This affects both applying network config (NNCP) and reporting state (NNS). Key changes: - Add kernelMode flag to nmstatectl package with -k flag for show/apply - Skip checkpoint/rollback/commit/probes in kernel mode (not supported) - Auto-detect NM absence at handler startup via nm.Version() - Guard NM version reporting in node controller - Update liveness probe to use -k flag when kernel-mode file exists - Add init container to ensure dbus socket path exists on nodes without dbus - Add feature Labels to all e2e handler tests for mode-based filtering - Make BeforeSuite work in kernel mode (no DHCP reset) - Add test-e2e-handler-kernel Makefile target with label filter - Add GitHub Actions CI workflow for kernel mode e2e on kind Kernel mode limitations: no checkpoint/rollback, no DHCP, volatile changes, cannot create interfaces via nmstatectl apply -k (nispor plugin limitation). Signed-off-by: Enrique Llorente <ellorent@redhat.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Enrique Llorente <ellorent@redhat.com>
1 parent bf2cc4d commit 034c590

43 files changed

Lines changed: 439 additions & 70 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
name: E2E Kernel Mode
2+
on:
3+
push:
4+
branches: [main]
5+
pull_request:
6+
branches: [main]
7+
jobs:
8+
e2e-kernel:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: actions/checkout@v4
12+
- uses: actions/setup-go@v5
13+
with:
14+
go-version-file: go.mod
15+
16+
- name: Write kind config
17+
run: |
18+
cat > /tmp/kind-config.yaml <<'EOF'
19+
kind: Cluster
20+
apiVersion: kind.x-k8s.io/v1alpha4
21+
containerdConfigPatches:
22+
- |-
23+
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."localhost:5000"]
24+
endpoint = ["http://kind-registry:5000"]
25+
nodes:
26+
- role: control-plane
27+
- role: worker
28+
- role: worker
29+
EOF
30+
31+
- name: Start local registry
32+
run: |
33+
docker run -d --restart=always -p 5000:5000 --name kind-registry registry:2
34+
35+
- name: Create kind cluster
36+
uses: helm/kind-action@v1
37+
with:
38+
cluster_name: kernel-mode-test
39+
node_image: kindest/node:v1.32.0
40+
config: /tmp/kind-config.yaml
41+
42+
- name: Connect registry to kind network
43+
run: |
44+
docker network connect kind kind-registry || true
45+
46+
- name: Create secondary docker networks and attach to kind nodes
47+
run: |
48+
docker network create --driver bridge kind-secondary-1
49+
docker network create --driver bridge kind-secondary-2
50+
for node in $(kind get nodes --name kernel-mode-test); do
51+
docker network connect kind-secondary-1 "$node"
52+
docker network connect kind-secondary-2 "$node"
53+
done
54+
55+
- name: Label kind worker nodes
56+
run: |
57+
for node in $(kubectl get nodes --no-headers -o custom-columns=NAME:.metadata.name | grep worker); do
58+
kubectl label node "$node" node-role.kubernetes.io/worker="" --overwrite
59+
done
60+
61+
- name: Deploy with cluster-sync
62+
run: |
63+
make cluster-up
64+
make cluster-sync
65+
env:
66+
KUBEVIRT_PROVIDER: external
67+
KUBECONFIG: /home/runner/.kube/config
68+
DEV_IMAGE_REGISTRY: localhost:5000
69+
IMAGE_BUILDER: docker
70+
KUBEVIRT_NUM_NODES: 3
71+
72+
- name: Run kernel mode e2e tests
73+
run: make test-e2e-handler-kernel
74+
env:
75+
KUBEVIRT_PROVIDER: external
76+
KUBECONFIG: /home/runner/.kube/config

Makefile

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,9 +205,18 @@ test-reporter:
205205
go run . --dry-run --fake=failure && \
206206
go run . --dry-run --fake=stale
207207

208+
# Features supported in kernel mode (no NetworkManager).
209+
# Only read-only NNS tests — nmstatectl kernel mode (-k) cannot create
210+
# interfaces via apply (nispor plugin limitation).
211+
KERNEL_MODE_FEATURES ?= Nodes || NNSDependencies
212+
208213
test-e2e-handler:
209214
KUBECONFIG=$(KUBECONFIG) OPERATOR_NAMESPACE=$(OPERATOR_NAMESPACE) MONITORING_NAMESPACE=$(MONITORING_NAMESPACE) $(GINKGO) $(e2e_test_args) ./test/e2e/handler ...
210215

216+
test-e2e-handler-kernel:
217+
KUBECONFIG=$(KUBECONFIG) OPERATOR_NAMESPACE=$(OPERATOR_NAMESPACE) MONITORING_NAMESPACE=$(MONITORING_NAMESPACE) \
218+
$(GINKGO) $(e2e_test_args) --label-filter="$(KERNEL_MODE_FEATURES)" ./test/e2e/handler ...
219+
211220
test-e2e-operator: manifests
212221
KUBECONFIG=$(KUBECONFIG) OPERATOR_NAMESPACE=$(OPERATOR_NAMESPACE) MONITORING_NAMESPACE=$(MONITORING_NAMESPACE) $(GINKGO) $(e2e_test_args) ./test/e2e/operator ...
213222

@@ -286,6 +295,7 @@ olm-push: bundle-push index-push
286295
check-gen \
287296
operator-sdk \
288297
test-e2e-handler \
298+
test-e2e-handler-kernel \
289299
test-e2e-operator \
290300
test-e2e \
291301
test-reporter\

cluster/up.sh

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,31 @@ set -ex
44

55
source ./cluster/kubevirtci.sh
66
kubevirtci::install
7+
source ./cluster/sync-common.sh
78

89
$(kubevirtci::path)/cluster-up/up.sh
910

1011
if [[ "$KUBEVIRT_PROVIDER" =~ ^(okd|ocp)-.*$$ ]]; then \
1112
while ! $(KUBECTL) get securitycontextconstraints; do sleep 1; done; \
1213
fi
1314

14-
echo 'Upgrading NetworkManager and enabling and starting up openvswitch'
15-
for node in $(./cluster/kubectl.sh get nodes --no-headers | awk '{print $1}'); do
16-
if [[ "$NM_VERSION" == "latest" ]]; then
17-
echo "Installing NetworkManager from copr networkmanager/NetworkManager-main"
18-
./cluster/cli.sh ssh ${node} -- sudo dnf install -y dnf-plugins-core
19-
./cluster/cli.sh ssh ${node} -- sudo dnf copr enable -y networkmanager/NetworkManager-main
20-
fi
21-
./cluster/cli.sh ssh ${node} -- sudo dnf upgrade -y NetworkManager --allowerasing
22-
./cluster/cli.sh ssh ${node} -- sudo systemctl daemon-reload
23-
./cluster/cli.sh ssh ${node} -- sudo systemctl enable openvswitch
24-
./cluster/cli.sh ssh ${node} -- sudo systemctl restart openvswitch
25-
# Newer kubevirtci has dhclient installed so we should enforce not using it to
26-
# keep using the NM internal DHCP client as we always have
27-
./cluster/cli.sh ssh ${node} -- sudo rm -f /etc/NetworkManager/conf.d/002-dhclient.conf
28-
./cluster/cli.sh ssh ${node} -- sudo systemctl restart NetworkManager
29-
done
15+
if isExternal; then
16+
echo 'Skipping NetworkManager/OVS setup for external provider'
17+
else
18+
echo 'Upgrading NetworkManager and enabling and starting up openvswitch'
19+
for node in $(./cluster/kubectl.sh get nodes --no-headers | awk '{print $1}'); do
20+
if [[ "$NM_VERSION" == "latest" ]]; then
21+
echo "Installing NetworkManager from copr networkmanager/NetworkManager-main"
22+
./cluster/cli.sh ssh ${node} -- sudo dnf install -y dnf-plugins-core
23+
./cluster/cli.sh ssh ${node} -- sudo dnf copr enable -y networkmanager/NetworkManager-main
24+
fi
25+
./cluster/cli.sh ssh ${node} -- sudo dnf upgrade -y NetworkManager --allowerasing
26+
./cluster/cli.sh ssh ${node} -- sudo systemctl daemon-reload
27+
./cluster/cli.sh ssh ${node} -- sudo systemctl enable openvswitch
28+
./cluster/cli.sh ssh ${node} -- sudo systemctl restart openvswitch
29+
# Newer kubevirtci has dhclient installed so we should enforce not using it to
30+
# keep using the NM internal DHCP client as we always have
31+
./cluster/cli.sh ssh ${node} -- sudo rm -f /etc/NetworkManager/conf.d/002-dhclient.conf
32+
./cluster/cli.sh ssh ${node} -- sudo systemctl restart NetworkManager
33+
done
34+
fi

cmd/handler/main.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import (
6363
"github.com/nmstate/kubernetes-nmstate/pkg/file"
6464
nmstatelog "github.com/nmstate/kubernetes-nmstate/pkg/log"
6565
"github.com/nmstate/kubernetes-nmstate/pkg/monitoring"
66+
"github.com/nmstate/kubernetes-nmstate/pkg/nm"
6667
"github.com/nmstate/kubernetes-nmstate/pkg/nmstatectl"
6768
"github.com/nmstate/kubernetes-nmstate/pkg/webhook"
6869
)
@@ -224,9 +225,29 @@ func setupWebhookEnvironment(mgr manager.Manager) error {
224225
return nil
225226
}
226227

228+
// detectAndSetKernelMode checks if NetworkManager is available and enables kernel mode if not.
229+
func detectAndSetKernelMode() {
230+
_, err := nm.Version()
231+
if err != nil {
232+
setupLog.Info("NetworkManager not available, enabling kernel mode",
233+
"error", err.Error())
234+
nmstatectl.SetKernelMode(true)
235+
} else {
236+
setupLog.Info("NetworkManager detected, using standard mode")
237+
}
238+
}
239+
227240
// setupHandlerEnvironment cleans up unavailableNodeCounts after unexpected restart,
228241
// configures the handler controllers and performs health checks
229242
func setupHandlerEnvironment(mgr manager.Manager) error {
243+
detectAndSetKernelMode()
244+
245+
if nmstatectl.IsKernelMode() {
246+
if err := file.Touch("/tmp/kernel-mode"); err != nil {
247+
setupLog.Error(err, "Failed to write kernel-mode flag file")
248+
}
249+
}
250+
230251
// Clean stale unavailable counts from node before starting controllers
231252
// Prevents deadlock after unexpected cluster reboot where nodes were
232253
// processing NNCP and left stale counts in etcd.

controllers/handler/node_controller.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,14 @@ func (r *NodeReconciler) getDependencyVersions() *nmstate.DependencyVersions {
128128
r.Log.Error(err, "failed retrieving handler nmstate version")
129129
}
130130

131-
hostNetworkManagerVersion, err := nm.Version()
132-
if err != nil {
133-
r.Log.Error(err, "error retrieving host Networkmanager version")
131+
var hostNetworkManagerVersion string
132+
if nmstatectl.IsKernelMode() {
133+
hostNetworkManagerVersion = "N/A (kernel mode)"
134+
} else {
135+
hostNetworkManagerVersion, err = nm.Version()
136+
if err != nil {
137+
r.Log.Error(err, "error retrieving host Networkmanager version")
138+
}
134139
}
135140

136141
return &nmstate.DependencyVersions{

controllers/operator/nmstate_controller_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -487,7 +487,7 @@ var _ = Describe("NMState controller reconcile", func() {
487487
ds := &appsv1.DaemonSet{}
488488
err := cl.Get(context.Background(), handlerKey, ds)
489489
Expect(err).ToNot(HaveOccurred())
490-
expectedCommand := "nmstatectl show -vv 2>&1"
490+
expectedCommand := "if [ -f /tmp/kernel-mode ]; then nmstatectl show -k -vv 2>&1; else nmstatectl show -vv 2>&1; fi"
491491
Expect(ds.Spec.Template.Spec.Containers[0].LivenessProbe.Exec.Command).To(ContainElement(expectedCommand))
492492
})
493493
})
@@ -515,7 +515,7 @@ var _ = Describe("NMState controller reconcile", func() {
515515
ds := &appsv1.DaemonSet{}
516516
err := cl.Get(context.Background(), handlerKey, ds)
517517
Expect(err).ToNot(HaveOccurred())
518-
expectedCommand := "nmstatectl show 2>&1"
518+
expectedCommand := "if [ -f /tmp/kernel-mode ]; then nmstatectl show -k 2>&1; else nmstatectl show 2>&1; fi"
519519
Expect(ds.Spec.Template.Spec.Containers[0].LivenessProbe.Exec.Command).To(ContainElement(expectedCommand))
520520
})
521521
})
@@ -542,7 +542,7 @@ var _ = Describe("NMState controller reconcile", func() {
542542
ds := &appsv1.DaemonSet{}
543543
err := cl.Get(context.Background(), handlerKey, ds)
544544
Expect(err).ToNot(HaveOccurred())
545-
expectedCommand := "nmstatectl show 2>&1"
545+
expectedCommand := "if [ -f /tmp/kernel-mode ]; then nmstatectl show -k 2>&1; else nmstatectl show 2>&1; fi"
546546
Expect(ds.Spec.Template.Spec.Containers[0].LivenessProbe.Exec.Command).To(ContainElement(expectedCommand))
547547
})
548548
})

deploy/handler/operator.yaml

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,19 @@ spec:
361361
tolerations: {{ toYaml .HandlerTolerations | nindent 8 }}
362362
affinity: {{ toYaml .HandlerAffinity | nindent 8 }}
363363
priorityClassName: system-node-critical
364+
initContainers:
365+
- name: ensure-dbus-socket
366+
image: {{ .HandlerImage }}
367+
imagePullPolicy: {{ .HandlerPullPolicy }}
368+
command:
369+
- sh
370+
- -c
371+
- "mkdir -p /host-run/dbus && test -e /host-run/dbus/system_bus_socket || touch /host-run/dbus/system_bus_socket"
372+
volumeMounts:
373+
- name: host-run
374+
mountPath: /host-run
375+
securityContext:
376+
privileged: true
364377
containers:
365378
- name: nmstate-handler
366379
args:
@@ -445,17 +458,19 @@ spec:
445458
command:
446459
- bash
447460
- -c
448-
- "nmstatectl show {{ .HandlerReadinessProbeExtraArg }} 2>&1"
461+
- "if [ -f /tmp/kernel-mode ]; then nmstatectl show -k {{ .HandlerReadinessProbeExtraArg }} 2>&1; else nmstatectl show {{ .HandlerReadinessProbeExtraArg }} 2>&1; fi"
449462
initialDelaySeconds: 60
450463
periodSeconds: 60
451464
timeoutSeconds: 10
452465
successThreshold: 1
453466
failureThreshold: 5
454467
volumes:
468+
- name: host-run
469+
hostPath:
470+
path: /run
455471
- name: dbus-socket
456472
hostPath:
457473
path: /run/dbus/system_bus_socket
458-
type: Socket
459474
- name: nmstate-lock
460475
hostPath:
461476
path: /var/k8s_nmstate

pkg/client/client.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,15 @@ func ApplyDesiredState(ctx context.Context, cli client.Client, desiredState shar
154154
return "Ignoring empty desired state", nil
155155
}
156156

157+
if nmstatectl.IsKernelMode() {
158+
log.Info("Kernel mode: applying desired state without checkpoint/rollback/probes")
159+
setOutput, err := nmstatectl.Set(desiredState, DesiredStateConfigurationTimeout)
160+
if err != nil {
161+
return setOutput, err
162+
}
163+
return fmt.Sprintf("setOutput: %s \n", setOutput), nil
164+
}
165+
157166
// Before apply we get the probes that are working fine, they should be
158167
// working fine after apply
159168
probes := probe.Select(ctx, cli)

pkg/nmstatectl/nmstatectl.go

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,20 @@ import (
2828
"time"
2929

3030
"github.com/pkg/errors"
31+
logf "sigs.k8s.io/controller-runtime/pkg/log"
3132
"sigs.k8s.io/yaml"
3233

3334
nmstate "github.com/nmstate/kubernetes-nmstate/api/shared"
3435
)
3536

36-
var debugMode bool
37+
var (
38+
debugMode bool
39+
kernelMode bool
40+
log = logf.Log.WithName("nmstatectl")
41+
)
42+
43+
func SetKernelMode(kernel bool) { kernelMode = kernel }
44+
func IsKernelMode() bool { return kernelMode }
3745

3846
const nmstateCommand = "nmstatectl"
3947

@@ -88,11 +96,20 @@ func nmstatectl(arguments []string) (string, error) {
8896
}
8997

9098
func ShowWithArgumentsAndOutputs(arguments []string, stdout, stderr io.Writer) error {
91-
return nmstatectlWithInputAndOutputs(append([]string{"show"}, arguments...), "", stdout, stderr)
99+
args := []string{"show"}
100+
if kernelMode {
101+
args = append(args, "-k")
102+
}
103+
args = append(args, arguments...)
104+
return nmstatectlWithInputAndOutputs(args, "", stdout, stderr)
92105
}
93106

94107
func Show() (string, error) {
95-
return nmstatectl([]string{"show"})
108+
args := []string{"show"}
109+
if kernelMode {
110+
args = append(args, "-k")
111+
}
112+
return nmstatectl(args)
96113
}
97114

98115
func Set(desiredState nmstate.State, timeout time.Duration) (string, error) {
@@ -103,17 +120,30 @@ func Set(desiredState nmstate.State, timeout time.Duration) (string, error) {
103120
if debugMode {
104121
args = append(args, "-vv")
105122
}
106-
args = append(args, "--no-commit", "--timeout", strconv.Itoa(int(timeout.Seconds())))
123+
if kernelMode {
124+
log.Info("Kernel mode: applying with -k flag, skipping --no-commit and --timeout (checkpoints not supported)")
125+
args = append(args, "-k")
126+
} else {
127+
args = append(args, "--no-commit", "--timeout", strconv.Itoa(int(timeout.Seconds())))
128+
}
107129

108130
setOutput, err := nmstatectlWithInput(args, string(desiredState.Raw))
109131
return setOutput, err
110132
}
111133

112134
func Commit() (string, error) {
135+
if kernelMode {
136+
log.Info("Kernel mode: skipping commit (checkpoints not supported)")
137+
return "commit skipped (kernel mode)", nil
138+
}
113139
return nmstatectl([]string{"commit"})
114140
}
115141

116142
func Rollback() error {
143+
if kernelMode {
144+
log.Info("Kernel mode: skipping rollback (checkpoints not supported)")
145+
return nil
146+
}
117147
_, err := nmstatectl([]string{"rollback"})
118148
if err != nil {
119149
return errors.Wrapf(err, "failed calling nmstatectl rollback")

0 commit comments

Comments
 (0)