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>
52 lines
1.3 KiB
Go
52 lines
1.3 KiB
Go
package kernelcat
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"testing"
|
|
)
|
|
|
|
func TestParseCatalogEmpty(t *testing.T) {
|
|
t.Parallel()
|
|
cat, err := ParseCatalog(nil)
|
|
if err != nil {
|
|
t.Fatalf("ParseCatalog(nil): %v", err)
|
|
}
|
|
if len(cat.Entries) != 0 {
|
|
t.Fatalf("entries = %d, want 0", len(cat.Entries))
|
|
}
|
|
}
|
|
|
|
func TestParseCatalogValid(t *testing.T) {
|
|
t.Parallel()
|
|
cat, err := ParseCatalog([]byte(`{"version":1,"entries":[{"name":"void-6.12","distro":"void","tarball_url":"https://example/v.tar.zst","tarball_sha256":"abc"}]}`))
|
|
if err != nil {
|
|
t.Fatalf("ParseCatalog: %v", err)
|
|
}
|
|
if cat.Version != 1 || len(cat.Entries) != 1 || cat.Entries[0].Name != "void-6.12" {
|
|
t.Fatalf("catalog = %+v", cat)
|
|
}
|
|
}
|
|
|
|
func TestCatalogLookup(t *testing.T) {
|
|
t.Parallel()
|
|
cat := Catalog{Entries: []CatEntry{{Name: "a"}, {Name: "b"}}}
|
|
if entry, err := cat.Lookup("b"); err != nil || entry.Name != "b" {
|
|
t.Fatalf("Lookup(b) = %+v, %v", entry, err)
|
|
}
|
|
if _, err := cat.Lookup("c"); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatalf("Lookup(missing) err = %v, want ErrNotExist", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadEmbeddedReturnsValidCatalog(t *testing.T) {
|
|
t.Parallel()
|
|
cat, err := LoadEmbedded()
|
|
if err != nil {
|
|
t.Fatalf("LoadEmbedded: %v", err)
|
|
}
|
|
if cat.Version != 1 {
|
|
t.Fatalf("embedded catalog.Version = %d, want 1", cat.Version)
|
|
}
|
|
// Embedded catalog starts empty; Phase 5 CI populates it.
|
|
}
|