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
5 changes: 5 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Contributing to Kerno

Check warning on line 1 in CONTRIBUTING.md

View workflow job for this annotation

GitHub Actions / Spell check

Unknown word (Kerno)

Thank you for your interest in contributing to Kerno! This document provides guidelines, full setup instructions, and best practices for contributors.

Check warning on line 3 in CONTRIBUTING.md

View workflow job for this annotation

GitHub Actions / Spell check

Unknown word (Kerno)

Expand All @@ -21,7 +21,7 @@

## Developer Certificate of Origin (DCO)

All contributions to Kerno must be signed off under the [Developer Certificate of Origin (DCO)](https://developercertificate.org/). This certifies that you wrote or have the right to submit the code you are contributing.

Check warning on line 24 in CONTRIBUTING.md

View workflow job for this annotation

GitHub Actions / Spell check

Unknown word (Kerno)

**Every commit must include a `Signed-off-by` line:**

Expand All @@ -32,7 +32,7 @@
You can do this automatically by committing with the `-s` flag:

```bash
git commit -s -m "feat: add syscall latency collector"

Check warning on line 35 in CONTRIBUTING.md

View workflow job for this annotation

GitHub Actions / Spell check

Unknown word (syscall)
```

---
Expand All @@ -48,15 +48,15 @@
| **Go** | 1.24 | 1.25+ | [install](https://go.dev/doc/install) |
| **make** | 4.0 | - | Build orchestration |

**Optional (for eBPF development and running Kerno):**

Check warning on line 51 in CONTRIBUTING.md

View workflow job for this annotation

GitHub Actions / Spell check

Unknown word (Kerno)

| Requirement | Minimum | Recommended | Notes |
|---|---|---|---|
| **Linux kernel** | 5.8 | 6.1+ | Must have `CONFIG_DEBUG_INFO_BTF=y` to run |
| **clang** | 14 | 17+ | For eBPF C compilation |
| **llvm** | 14 | 17+ | `llvm-strip` used by bpf2go |
| **libbpf-dev** | 0.8 | 1.0+ | BPF CO-RE headers |

Check warning on line 58 in CONTRIBUTING.md

View workflow job for this annotation

GitHub Actions / Spell check

Unknown word (libbpf)
| **bpftool** | - | latest | BTF inspection and debugging |

Check warning on line 59 in CONTRIBUTING.md

View workflow job for this annotation

GitHub Actions / Spell check

Unknown word (bpftool)

### Step 1 - Install System Dependencies (Optional for Go-only dev)

Expand All @@ -66,7 +66,7 @@
sudo apt-get update
sudo apt-get install -y \
clang llvm llvm-dev \
libbpf-dev \

Check warning on line 69 in CONTRIBUTING.md

View workflow job for this annotation

GitHub Actions / Spell check

Unknown word (libbpf)
linux-headers-$(uname -r) \
linux-tools-$(uname -r) linux-tools-common \
make gcc pkg-config \
Expand All @@ -78,9 +78,9 @@
```bash
sudo dnf install -y \
clang llvm llvm-devel \
libbpf-devel \

Check warning on line 81 in CONTRIBUTING.md

View workflow job for this annotation

GitHub Actions / Spell check

Unknown word (libbpf)
kernel-headers kernel-devel \
bpftool \

Check warning on line 83 in CONTRIBUTING.md

View workflow job for this annotation

GitHub Actions / Spell check

Unknown word (bpftool)
make gcc pkg-config \
git curl
```
Expand Down Expand Up @@ -274,6 +274,7 @@
- `internal/config` - Configuration parsing + validation (8 table-driven tests)
- `internal/bpf` - Event binary round-trip, helper methods, type consistency
- `internal/collector` - Registry lifecycle, signal aggregation
- `internal/preflight` - Host prerequisite validation (kernel, BTF, caps, port)

---

Expand Down Expand Up @@ -305,6 +306,7 @@
│ ├── cli/ # Cobra CLI commands
│ │ ├── root.go # Root command, flags, logger init
│ │ ├── doctor.go # `kerno doctor` command
│ │ ├── preflight.go # `kerno preflight` command
│ │ ├── version.go # `kerno version` command
│ │ └── start.go # `kerno start` daemon command
│ ├── collector/ # Signal collection + aggregation
Expand All @@ -314,6 +316,9 @@
│ │ └── config.go # Config struct, defaults, validation
│ └── version/ # Build metadata
│ └── version.go # Version, commit, date via ldflags
│ └── preflight/ # Host prerequisite validation
│ ├── checks.go # 10 check functions + RunAll
│ └── checks_test.go # Fixture-based unit tests
├── .github/
│ ├── workflows/
│ │ ├── ci.yml # Lint, test, build, BPF, Docker jobs
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ The same binary, the same command. No Kubernetes required.

```bash
curl -sfL https://raw.githubusercontent.com/optiqor/kerno/main/scripts/install.sh | sudo bash
sudo kerno preflight # validate host prerequisites
sudo kerno doctor
```

Expand Down
22 changes: 22 additions & 0 deletions deploy/helm/kerno/templates/daemonset.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,28 @@ spec:
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.preflight.initContainer }}
initContainers:
- name: preflight
image: {{ include "kerno.image" . }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
args: ["preflight"]
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
volumeMounts:
- name: sys-kernel-btf
mountPath: /sys/kernel/btf
readOnly: true
- name: sys-kernel-debug
mountPath: /sys/kernel/debug
readOnly: true
- name: proc
mountPath: /proc
readOnly: true
- name: sys-fs-cgroup
mountPath: /sys/fs/cgroup
readOnly: true
{{- end }}
containers:
- name: kerno
image: {{ include "kerno.image" . }}
Expand Down
75 changes: 75 additions & 0 deletions deploy/helm/kerno/templates/preflight-job.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
{{- if .Values.preflight.enabled }}
apiVersion: batch/v1
kind: Job
metadata:
name: kerno-preflight
namespace: {{ .Release.Namespace }}
labels:
{{- include "kerno.labels" . | nindent 4 }}
app.kubernetes.io/component: preflight
annotations:
"helm.sh/hook": pre-install,pre-upgrade
"helm.sh/hook-weight": "-5"
"helm.sh/hook-delete-policy": hook-succeeded,before-hook-creation
spec:
backoffLimit: 0
template:
spec:
restartPolicy: Never
serviceAccountName: kerno
hostPID: true
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
containers:
- name: preflight
image: {{ include "kerno.image" . }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
args: ["preflight", "--output", "json"]
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
resources:
requests:
cpu: 50m
memory: 32Mi
limits:
cpu: 100m
memory: 64Mi
volumeMounts:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the job mounts btf, proc, and cgroup, but CheckTracefs (checks.go:362) looks for /sys/kernel/tracing or /sys/kernel/debug/tracing, and neither is mounted here. the daemonset mounts /sys/kernel/debug (daemonset.yaml), so the actual daemon has it but this pre-install hook doesn't, which means the hook reports a tracefs problem on every healthy node and can block install. add the same /sys/kernel/debug (and tracing) mounts so the job's checks match what the daemon actually runs with.

- name: sys-kernel-btf
mountPath: /sys/kernel/btf
readOnly: true
- name: sys-kernel-debug
mountPath: /sys/kernel/debug
readOnly: true
- name: proc
mountPath: /proc
readOnly: true
- name: sys-fs-cgroup
mountPath: /sys/fs/cgroup
readOnly: true
volumes:
- name: sys-kernel-btf
hostPath:
path: /sys/kernel/btf
type: Directory
- name: sys-kernel-debug
hostPath:
path: /sys/kernel/debug
type: Directory
- name: proc
hostPath:
path: /proc
type: Directory
- name: sys-fs-cgroup
hostPath:
path: /sys/fs/cgroup
type: Directory
{{- end }}
12 changes: 12 additions & 0 deletions deploy/helm/kerno/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,15 @@ securityContext:
# Host path prefix — Kerno mounts host paths under /host/ to avoid conflicts.
# All pollers read from /host/proc, /host/sys/fs/cgroup, etc.
hostPathPrefix: /host

# Preflight checks — validate host prerequisites before starting kerno.
#
# NOTE: The pre-install hook Job runs on a single scheduled node, so it
# validates only that one node — not every node the DaemonSet lands on. On a
# mixed-kernel cluster the hook can pass while some nodes still fail at start.
# Enable initContainer for a per-node gate that checks every node.
preflight:
# Enable the Helm pre-install hook Job (validates one node).
enabled: false
# Add preflight as an init container in the DaemonSet (validates every node).
initContainer: false
1 change: 1 addition & 0 deletions deploy/systemd/kerno.service
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Wants=network-online.target

[Service]
Type=simple
ExecStartPre=/usr/local/bin/kerno preflight
ExecStart=/usr/local/bin/kerno start
Restart=on-failure
RestartSec=5s
Expand Down
195 changes: 195 additions & 0 deletions internal/cli/preflight.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
// Copyright 2026 Optiqor contributors
// SPDX-License-Identifier: Apache-2.0

package cli

import (
"encoding/json"
"fmt"
"os"

"github.com/spf13/cobra"

"github.com/optiqor/kerno/internal/preflight"
)

func newPreflightCmd() *cobra.Command {
var (
output string
outputDir string
promAddr string
)

cmd := &cobra.Command{
Use: "preflight",
Short: "Validate host prerequisites for running kerno",
Long: `Preflight runs every host prerequisite check and reports each as PASS / FAIL / WARN.

Use this before "kerno doctor" to verify the host can run kerno. Each check
includes a remediation hint when it fails.

Exit codes:
0 All checks passed (warnings are printed to stderr but don't block)
1 One or more checks failed`,
Example: ` # Check if this host can run kerno
sudo kerno preflight

# Machine-readable for Helm hooks and CI
kerno preflight --output json

# Custom output directory and Prometheus port
sudo kerno preflight --output-dir /data/kerno --prom-addr :9091`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
// Inherit --output from root if not set via preflight flag.
if output == "" {
output, _ = cmd.Root().PersistentFlags().GetString("output")
}

// Build CheckOptions from flags + config.
opts := preflight.CheckOptions{}
if outputDir != "" {
opts.OutputDir = outputDir
}
if promAddr != "" {
opts.PromAddr = promAddr
} else if cfg != nil && cfg.Prometheus.Addr != "" {
opts.PromAddr = cfg.Prometheus.Addr
}

// Run all preflight checks.
results := preflight.RunAll(opts)

// Render output.
switch output {
case "json":
return renderPreflightJSON(cmd, results)
default:
return renderPreflightPretty(cmd, results)
}
},
}

flags := cmd.Flags()
flags.StringVarP(&output, "output", "o", "", "output format: pretty, json")
flags.StringVar(&outputDir, "output-dir", "", "output directory to check (default: /var/log/kerno)")
flags.StringVar(&promAddr, "prom-addr", "", "Prometheus address to check (default from config)")

return cmd
}

// preflightSummary counts the results by status.
type preflightSummary struct {
Pass int `json:"pass"`
Warn int `json:"warn"`
Fail int `json:"fail"`
}

// preflightReport is the JSON output structure.
type preflightReport struct {
Checks []preflight.Result `json:"checks"`
Summary preflightSummary `json:"summary"`
Ready bool `json:"ready"`
}

func summarize(results []preflight.Result) preflightSummary {
var s preflightSummary
for i := range results {
switch results[i].Status {
case preflight.StatusPass:
s.Pass++
case preflight.StatusWarn:
s.Warn++
case preflight.StatusFail:
s.Fail++
}
}
return s
}

func renderPreflightJSON(cmd *cobra.Command, results []preflight.Result) error {
s := summarize(results)
report := preflightReport{
Checks: results,
Summary: s,
Ready: s.Fail == 0,
}

enc := json.NewEncoder(cmd.OutOrStdout())
enc.SetIndent("", " ")
if err := enc.Encode(report); err != nil {
return fmt.Errorf("encoding JSON: %w", err)
}

if s.Fail > 0 {
return &exitError{code: 1}
}
return nil
}

func renderPreflightPretty(cmd *cobra.Command, results []preflight.Result) error {
w := cmd.OutOrStdout()
noColor := os.Getenv("NO_COLOR") != "" || !isTerminal()

fmt.Fprintln(w, "==> Kerno preflight check")
fmt.Fprintln(w)

for i := range results {
r := &results[i]
tag := formatStatusTag(r.Status, noColor)
fmt.Fprintf(w, "%s %s\n", tag, r.Message)
}

s := summarize(results)
fmt.Fprintln(w)

var verdict string
if s.Fail == 0 {
verdict = "ready to start"
} else {
verdict = "not ready"
}
fmt.Fprintf(w, "Result: %d PASS, %d WARN, %d FAIL → %s\n", s.Pass, s.Warn, s.Fail, verdict)

// Print remediation hints for failures and warnings to stderr.
var hints []string
for i := range results {
if results[i].Detail != "" && results[i].Status != preflight.StatusPass {
hints = append(hints, fmt.Sprintf(" %s: %s", results[i].Name, results[i].Detail))
}
}
if len(hints) > 0 {
fmt.Fprintln(os.Stderr)
fmt.Fprintln(os.Stderr, "Remediation hints:")
for _, h := range hints {
fmt.Fprintln(os.Stderr, h)
}
}

if s.Fail > 0 {
return &exitError{code: 1}
}
return nil
}

// formatStatusTag returns a colored [PASS], [WARN], or [FAIL] tag.
func formatStatusTag(s preflight.Status, noColor bool) string {
label := s.String()

if noColor {
return "[" + label + "]"
}

var color string
switch s {
case preflight.StatusPass:
color = "\033[32m" // green
case preflight.StatusWarn:
color = "\033[33m" // yellow
case preflight.StatusFail:
color = "\033[31m" // red
}
reset := "\033[0m"

return color + "[" + label + "]" + reset
}
4 changes: 3 additions & 1 deletion internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ and copy-paste fix steps.`,
watchCmd := newWatchCmd()
auditCmd := newAuditCmd()
chaosCmd := newChaosCmd()
preflightCmd := newPreflightCmd()
versionCmd := newVersionCmd()
completionCmd := newCompletionCmd()

Expand All @@ -98,10 +99,11 @@ and copy-paste fix steps.`,
auditCmd.GroupID = "observe"
startCmd.GroupID = "ops"
chaosCmd.GroupID = "ops"
preflightCmd.GroupID = "ops"
versionCmd.GroupID = "ops"
completionCmd.GroupID = "ops"

root.AddCommand(doctorCmd, explainCmd, predictCmd, traceCmd, watchCmd, auditCmd, startCmd, chaosCmd, versionCmd, completionCmd)
root.AddCommand(doctorCmd, explainCmd, predictCmd, traceCmd, watchCmd, auditCmd, startCmd, chaosCmd, preflightCmd, versionCmd, completionCmd)

return root
}
Expand Down
Loading