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:
parent
7192ba24ae
commit
f0668ee598
13 changed files with 711 additions and 4 deletions
|
|
@ -1600,10 +1600,33 @@ func newKernelCommand() *cobra.Command {
|
|||
newKernelShowCommand(),
|
||||
newKernelRmCommand(),
|
||||
newKernelImportCommand(),
|
||||
newKernelPullCommand(),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newKernelPullCommand() *cobra.Command {
|
||||
var force bool
|
||||
cmd := &cobra.Command{
|
||||
Use: "pull <name>",
|
||||
Short: "Download a cataloged kernel bundle",
|
||||
Args: exactArgsUsage(1, "usage: banger kernel pull <name> [--force]"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
layout, _, err := ensureDaemon(cmd.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result, err := rpc.Call[api.KernelShowResult](cmd.Context(), layout.SocketPath, "kernel.pull", api.KernelPullParams{Name: args[0], Force: force})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return printJSON(cmd.OutOrStdout(), result.Entry)
|
||||
},
|
||||
}
|
||||
cmd.Flags().BoolVar(&force, "force", false, "re-pull even if already present")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newKernelImportCommand() *cobra.Command {
|
||||
var params api.KernelImportParams
|
||||
cmd := &cobra.Command{
|
||||
|
|
@ -1639,15 +1662,23 @@ func newKernelImportCommand() *cobra.Command {
|
|||
}
|
||||
|
||||
func newKernelListCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
var available bool
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List locally available kernels",
|
||||
Args: noArgsUsage("usage: banger kernel list"),
|
||||
Short: "List kernels (local by default, or --available for the catalog)",
|
||||
Args: noArgsUsage("usage: banger kernel list [--available]"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
layout, _, err := ensureDaemon(cmd.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if available {
|
||||
result, err := rpc.Call[api.KernelCatalogResult](cmd.Context(), layout.SocketPath, "kernel.catalog", api.Empty{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return printKernelCatalogTable(cmd.OutOrStdout(), result.Entries)
|
||||
}
|
||||
result, err := rpc.Call[api.KernelListResult](cmd.Context(), layout.SocketPath, "kernel.list", api.Empty{})
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -1655,6 +1686,8 @@ func newKernelListCommand() *cobra.Command {
|
|||
return printKernelListTable(cmd.OutOrStdout(), result.Entries)
|
||||
},
|
||||
}
|
||||
cmd.Flags().BoolVar(&available, "available", false, "show the built-in catalog (with pulled/available status) instead of local entries")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newKernelShowCommand() *cobra.Command {
|
||||
|
|
@ -1717,6 +1750,53 @@ func printKernelListTable(out anyWriter, entries []api.KernelEntry) error {
|
|||
return w.Flush()
|
||||
}
|
||||
|
||||
func printKernelCatalogTable(out anyWriter, entries []api.KernelCatalogEntry) error {
|
||||
w := tabwriter.NewWriter(out, 0, 8, 2, ' ', 0)
|
||||
if _, err := fmt.Fprintln(w, "NAME\tDISTRO\tARCH\tKERNEL\tSIZE\tSTATE"); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, entry := range entries {
|
||||
state := "available"
|
||||
if entry.Pulled {
|
||||
state = "pulled"
|
||||
}
|
||||
if _, err := fmt.Fprintf(
|
||||
w,
|
||||
"%s\t%s\t%s\t%s\t%s\t%s\n",
|
||||
entry.Name,
|
||||
dashIfEmpty(entry.Distro),
|
||||
dashIfEmpty(entry.Arch),
|
||||
dashIfEmpty(entry.KernelVersion),
|
||||
humanSize(entry.SizeBytes),
|
||||
state,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return w.Flush()
|
||||
}
|
||||
|
||||
func humanSize(bytes int64) string {
|
||||
if bytes <= 0 {
|
||||
return "-"
|
||||
}
|
||||
const (
|
||||
kib = 1024
|
||||
mib = 1024 * kib
|
||||
gib = 1024 * mib
|
||||
)
|
||||
switch {
|
||||
case bytes >= gib:
|
||||
return fmt.Sprintf("%.1fGiB", float64(bytes)/float64(gib))
|
||||
case bytes >= mib:
|
||||
return fmt.Sprintf("%.1fMiB", float64(bytes)/float64(mib))
|
||||
case bytes >= kib:
|
||||
return fmt.Sprintf("%.1fKiB", float64(bytes)/float64(kib))
|
||||
default:
|
||||
return fmt.Sprintf("%dB", bytes)
|
||||
}
|
||||
}
|
||||
|
||||
func dashIfEmpty(s string) string {
|
||||
if strings.TrimSpace(s) == "" {
|
||||
return "-"
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ func TestKernelCommandExposesSubcommands(t *testing.T) {
|
|||
for _, sub := range kernel.Commands() {
|
||||
names = append(names, sub.Name())
|
||||
}
|
||||
want := []string{"import", "list", "rm", "show"}
|
||||
want := []string{"import", "list", "pull", "rm", "show"}
|
||||
if !reflect.DeepEqual(names, want) {
|
||||
t.Fatalf("kernel subcommands = %v, want %v", names, want)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue