Phase 4: remote catalog + banger kernel pull

Introduces the headline feature of the kernel catalog: pulling a kernel
bundle over HTTP without any local build step.

Catalog format (internal/kernelcat/catalog.go):
 - Catalog { Version, Entries } + CatEntry { Name, Distro, Arch,
   KernelVersion, TarballURL, TarballSHA256, SizeBytes, Description }.
 - catalog.json is embedded via go:embed and ships with each banger
   binary. It starts empty (Phase 5's CI pipeline will populate it).
 - Lookup(name) returns the matching entry or os.ErrNotExist.

Fetch (internal/kernelcat/fetch.go):
 - HTTP GET with streaming SHA256 over the response body.
 - zstd-decode (github.com/klauspost/compress/zstd) -> tar extract into
   <kernelsDir>/<name>/.
 - Hardens against path-traversal tarball entries (members whose
   normalised path escapes the target dir, and unsafe symlink
   targets) and sha256-mismatch downloads; any failure removes the
   partially-populated target dir.
 - Regular files, directories, and safe symlinks are supported; other
   tar types (hardlinks, devices, fifos) are silently skipped.
 - After extraction, recomputes sha256 over the on-disk vmlinux and
   writes the manifest with Source="pull:<url>".

Daemon methods (internal/daemon/kernels.go):
 - KernelPull(ctx, {Name, Force}) - lookup in embedded catalog, refuse
   overwrite unless Force, delegate to kernelcat.Fetch.
 - KernelCatalog(ctx) - return the embedded catalog annotated per-entry
   with whether it has been pulled locally.

RPC: kernel.pull, kernel.catalog dispatch cases.

CLI:
 - `banger kernel pull <name> [--force]`.
 - `banger kernel list --available` prints the catalog with a
   pulled/available STATE column and a human-readable size.

Tests: fetch round-trip (extract + manifest + sha256), sha256 mismatch
rejection with cleanup, missing-vmlinux rejection, path-traversal
rejection, HTTP error propagation, catalog parsing, lookup,
pulled-status reconciliation. All 20 packages green.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Thales Maciel 2026-04-16 15:05:42 -03:00
parent 7192ba24ae
commit f0668ee598
No known key found for this signature in database
GPG key ID: 33112E6833C34679
13 changed files with 711 additions and 4 deletions

View file

@ -114,6 +114,65 @@ func (d *Daemon) KernelImport(ctx context.Context, params api.KernelImportParams
return kernelEntryToAPI(stored), nil
}
// KernelPull downloads a catalog entry by name into the local catalog. It
// refuses to overwrite an existing entry unless params.Force is set.
func (d *Daemon) KernelPull(ctx context.Context, params api.KernelPullParams) (api.KernelEntry, error) {
name := strings.TrimSpace(params.Name)
if err := kernelcat.ValidateName(name); err != nil {
return api.KernelEntry{}, err
}
if !params.Force {
if _, err := kernelcat.ReadLocal(d.layout.KernelsDir, name); err == nil {
return api.KernelEntry{}, fmt.Errorf("kernel %q already pulled; pass --force to re-pull", name)
} else if !os.IsNotExist(err) {
return api.KernelEntry{}, err
}
}
catalog, err := kernelcat.LoadEmbedded()
if err != nil {
return api.KernelEntry{}, err
}
catEntry, err := catalog.Lookup(name)
if err != nil {
return api.KernelEntry{}, fmt.Errorf("kernel %q not in catalog (run 'banger kernel list --available' to browse)", name)
}
stored, err := kernelcat.Fetch(ctx, nil, d.layout.KernelsDir, catEntry)
if err != nil {
return api.KernelEntry{}, err
}
return kernelEntryToAPI(stored), nil
}
// KernelCatalog returns every entry from the embedded catalog annotated
// with whether it has already been pulled locally.
func (d *Daemon) KernelCatalog(_ context.Context) (api.KernelCatalogResult, error) {
catalog, err := kernelcat.LoadEmbedded()
if err != nil {
return api.KernelCatalogResult{}, err
}
local, _ := kernelcat.ListLocal(d.layout.KernelsDir)
pulled := make(map[string]bool, len(local))
for _, entry := range local {
pulled[entry.Name] = true
}
result := api.KernelCatalogResult{Entries: make([]api.KernelCatalogEntry, 0, len(catalog.Entries))}
for _, entry := range catalog.Entries {
result.Entries = append(result.Entries, api.KernelCatalogEntry{
Name: entry.Name,
Distro: entry.Distro,
Arch: entry.Arch,
KernelVersion: entry.KernelVersion,
SizeBytes: entry.SizeBytes,
Description: entry.Description,
Pulled: pulled[entry.Name],
})
}
return result, nil
}
// inferKernelVersion makes a best-effort guess at the kernel version from
// the source filename (e.g. "vmlinux-6.12.79_1") or falls back to the
// modules directory basename. Returns "" if nothing looks useful.