Phase 1: local kernel catalog scaffolding

Introduces a read/write kernel catalog on disk without any network
dependency, so later phases (image register --kernel-ref, import, pull)
can build on a working foundation.

Layout: adds KernelsDir to paths.Layout, ensured under
~/.local/state/banger/kernels/. Each cataloged kernel lives at
<KernelsDir>/<name>/ with a manifest.json alongside vmlinux and optional
initrd.img / modules/.

New internal/kernelcat package owns the disk format:
- Entry (Name, Distro, Arch, KernelVersion, SHA256, Source, ImportedAt)
- ValidateName (alphanumeric + dots/hyphens/underscores, no traversal)
- ReadLocal / ListLocal / WriteLocal / DeleteLocal
- SumFile helper

The daemon exposes three RPC methods dispatched in daemon.go:
kernel.list, kernel.show, kernel.delete. Implementations live in a new
internal/daemon/kernels.go and are thin wrappers over kernelcat using
d.layout.KernelsDir.

CLI: new top-level `banger kernel` with list / show / rm subcommands
mirroring the image-command pattern (ensureDaemon, RPC call, table or
JSON output). No sudo required — kernel ops are user-space only.

Users can now manually populate ~/.local/state/banger/kernels/<name>/
and see it via `banger kernel list`. Phase 2 wires --kernel-ref into
image register; Phase 3 adds `banger kernel import`; Phase 4 adds
remote pulls.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Thales Maciel 2026-04-16 14:21:10 -03:00
parent ca4865447c
commit 83cc3aee15
No known key found for this signature in database
GPG key ID: 33112E6833C34679
9 changed files with 691 additions and 3 deletions

View file

@ -19,6 +19,8 @@ import (
"banger/internal/model"
"banger/internal/system"
"banger/internal/toolingplan"
"github.com/spf13/cobra"
)
func TestNewBangerCommandHasExpectedSubcommands(t *testing.T) {
@ -27,7 +29,7 @@ func TestNewBangerCommandHasExpectedSubcommands(t *testing.T) {
for _, sub := range cmd.Commands() {
names = append(names, sub.Name())
}
want := []string{"daemon", "doctor", "image", "internal", "ps", "version", "vm"}
want := []string{"daemon", "doctor", "image", "internal", "kernel", "ps", "version", "vm"}
if !reflect.DeepEqual(names, want) {
t.Fatalf("subcommands = %v, want %v", names, want)
}
@ -57,6 +59,28 @@ func TestVersionCommandPrintsBuildInfo(t *testing.T) {
}
}
func TestKernelCommandExposesSubcommands(t *testing.T) {
cmd := NewBangerCommand()
var kernel *cobra.Command
for _, sub := range cmd.Commands() {
if sub.Name() == "kernel" {
kernel = sub
break
}
}
if kernel == nil {
t.Fatalf("kernel command missing from root")
}
names := []string{}
for _, sub := range kernel.Commands() {
names = append(names, sub.Name())
}
want := []string{"list", "rm", "show"}
if !reflect.DeepEqual(names, want) {
t.Fatalf("kernel subcommands = %v, want %v", names, want)
}
}
func TestLegacyRemovedCommandIsRejected(t *testing.T) {
cmd := NewBangerCommand()
cmd.SetArgs([]string{"tui"})