README gets a top-level Updating section; docs/privileges.md gains a step-by-step trust-model writeup of `banger update`. The new scripts/publish-banger-release.sh drives the manual release cut: build, tar, sha256sum, cosign sign-blob, verify against the embedded public key, jq-merge into manifest.json, rclone upload to the R2 bucket. Refuses outright if the embedded key is still the placeholder so we can't accidentally publish an unverifiable release. Also folds in gofmt drift accumulated across the updater package and a few sibling files. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
309 lines
12 KiB
Go
309 lines
12 KiB
Go
package daemon
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"banger/internal/api"
|
|
"banger/internal/buildinfo"
|
|
"banger/internal/rpc"
|
|
)
|
|
|
|
// handler is the signature every RPC method dispatches through. Keeps
|
|
// Daemon.dispatch a one-liner — lookup + invoke — instead of the old
|
|
// ~240-line `switch`. Handlers close over a `*Daemon` parameter at
|
|
// call time (passed by the driver) rather than baked into the map,
|
|
// so tests that stand up a *Daemon with custom wiring re-use the
|
|
// same table without re-registering anything.
|
|
type handler func(ctx context.Context, d *Daemon, req rpc.Request) rpc.Response
|
|
|
|
// paramHandler wraps the common "decode params of type P, call
|
|
// service returning (R, error), wrap R" flow that 28 of 34 methods
|
|
// follow. Compile-time type-safe — no reflection. P and R are
|
|
// deduced from the function literal passed in, so per-handler
|
|
// registration reads as "what's the RPC shape + what's the service
|
|
// call" and nothing else.
|
|
func paramHandler[P any, R any](call func(ctx context.Context, d *Daemon, p P) (R, error)) handler {
|
|
return func(ctx context.Context, d *Daemon, req rpc.Request) rpc.Response {
|
|
p, err := rpc.DecodeParams[P](req)
|
|
if err != nil {
|
|
return rpc.NewError("bad_request", err.Error())
|
|
}
|
|
result, err := call(ctx, d, p)
|
|
return marshalResultOrError(result, err)
|
|
}
|
|
}
|
|
|
|
// noParamHandler is the decode-free variant for RPC methods that
|
|
// take no params (ping, shutdown, *.list, kernel.catalog).
|
|
func noParamHandler[R any](call func(ctx context.Context, d *Daemon) (R, error)) handler {
|
|
return func(ctx context.Context, d *Daemon, _ rpc.Request) rpc.Response {
|
|
result, err := call(ctx, d)
|
|
return marshalResultOrError(result, err)
|
|
}
|
|
}
|
|
|
|
// rpcHandlers maps every supported method name to its handler. Adding
|
|
// or removing a method is a single-line diff here — unlike the old
|
|
// switch, there's no four-line decode/call/wrap boilerplate to copy.
|
|
// The four special-case handlers (vm.logs, vm.ssh, ping, shutdown)
|
|
// live below the map; they need pre-service validation or raw result
|
|
// encoding that the generic wrapper can't express.
|
|
var rpcHandlers = map[string]handler{
|
|
"ping": pingHandler,
|
|
"shutdown": shutdownHandler,
|
|
"daemon.operations.list": noParamHandler(daemonOperationsListDispatch),
|
|
|
|
"vm.create": paramHandler(vmCreateDispatch),
|
|
"vm.create.begin": paramHandler(vmCreateBeginDispatch),
|
|
"vm.create.status": paramHandler(vmCreateStatusDispatch),
|
|
"vm.create.cancel": paramHandler(vmCreateCancelDispatch),
|
|
"vm.list": noParamHandler(vmListDispatch),
|
|
"vm.show": paramHandler(vmShowDispatch),
|
|
"vm.start": paramHandler(vmStartDispatch),
|
|
"vm.stop": paramHandler(vmStopDispatch),
|
|
"vm.kill": paramHandler(vmKillDispatch),
|
|
"vm.restart": paramHandler(vmRestartDispatch),
|
|
"vm.delete": paramHandler(vmDeleteDispatch),
|
|
"vm.set": paramHandler(vmSetDispatch),
|
|
"vm.stats": paramHandler(vmStatsDispatch),
|
|
"vm.logs": vmLogsHandler,
|
|
"vm.ssh": vmSSHHandler,
|
|
"vm.health": paramHandler(vmHealthDispatch),
|
|
"vm.ping": paramHandler(vmPingDispatch),
|
|
"vm.ports": paramHandler(vmPortsDispatch),
|
|
|
|
"vm.workspace.prepare": paramHandler(workspacePrepareDispatch),
|
|
"vm.workspace.export": paramHandler(workspaceExportDispatch),
|
|
|
|
"image.list": noParamHandler(imageListDispatch),
|
|
"image.show": paramHandler(imageShowDispatch),
|
|
"image.register": paramHandler(imageRegisterDispatch),
|
|
"image.promote": paramHandler(imagePromoteDispatch),
|
|
"image.delete": paramHandler(imageDeleteDispatch),
|
|
"image.pull": paramHandler(imagePullDispatch),
|
|
"image.cache.prune": paramHandler(imageCachePruneDispatch),
|
|
|
|
"kernel.list": noParamHandler(kernelListDispatch),
|
|
"kernel.show": paramHandler(kernelShowDispatch),
|
|
"kernel.delete": paramHandler(kernelDeleteDispatch),
|
|
"kernel.import": paramHandler(kernelImportDispatch),
|
|
"kernel.pull": paramHandler(kernelPullDispatch),
|
|
"kernel.catalog": noParamHandler(kernelCatalogDispatch),
|
|
}
|
|
|
|
// ---- Service-call adapters (kept thin; the interesting shape is up
|
|
// ---- in the `paramHandler` generic. These exist so the map entries
|
|
// ---- stay readable at a glance.)
|
|
|
|
func vmCreateDispatch(ctx context.Context, d *Daemon, p api.VMCreateParams) (api.VMShowResult, error) {
|
|
vm, err := d.vm.CreateVM(ctx, p)
|
|
return api.VMShowResult{VM: vm}, err
|
|
}
|
|
|
|
func vmCreateBeginDispatch(ctx context.Context, d *Daemon, p api.VMCreateParams) (api.VMCreateBeginResult, error) {
|
|
op, err := d.vm.BeginVMCreate(ctx, p)
|
|
return api.VMCreateBeginResult{Operation: op}, err
|
|
}
|
|
|
|
func vmCreateStatusDispatch(ctx context.Context, d *Daemon, p api.VMCreateStatusParams) (api.VMCreateStatusResult, error) {
|
|
op, err := d.vm.VMCreateStatus(ctx, p.ID)
|
|
return api.VMCreateStatusResult{Operation: op}, err
|
|
}
|
|
|
|
func vmCreateCancelDispatch(ctx context.Context, d *Daemon, p api.VMCreateStatusParams) (api.Empty, error) {
|
|
return api.Empty{}, d.vm.CancelVMCreate(ctx, p.ID)
|
|
}
|
|
|
|
func vmListDispatch(ctx context.Context, d *Daemon) (api.VMListResult, error) {
|
|
vms, err := d.store.ListVMs(ctx)
|
|
return api.VMListResult{VMs: vms}, err
|
|
}
|
|
|
|
func vmShowDispatch(ctx context.Context, d *Daemon, p api.VMRefParams) (api.VMShowResult, error) {
|
|
vm, err := d.vm.FindVM(ctx, p.IDOrName)
|
|
return api.VMShowResult{VM: vm}, err
|
|
}
|
|
|
|
func vmStartDispatch(ctx context.Context, d *Daemon, p api.VMRefParams) (api.VMShowResult, error) {
|
|
vm, err := d.vm.StartVM(ctx, p.IDOrName)
|
|
return api.VMShowResult{VM: vm}, err
|
|
}
|
|
|
|
func vmStopDispatch(ctx context.Context, d *Daemon, p api.VMRefParams) (api.VMShowResult, error) {
|
|
vm, err := d.vm.StopVM(ctx, p.IDOrName)
|
|
return api.VMShowResult{VM: vm}, err
|
|
}
|
|
|
|
func vmKillDispatch(ctx context.Context, d *Daemon, p api.VMKillParams) (api.VMShowResult, error) {
|
|
vm, err := d.vm.KillVM(ctx, p)
|
|
return api.VMShowResult{VM: vm}, err
|
|
}
|
|
|
|
func vmRestartDispatch(ctx context.Context, d *Daemon, p api.VMRefParams) (api.VMShowResult, error) {
|
|
vm, err := d.vm.RestartVM(ctx, p.IDOrName)
|
|
return api.VMShowResult{VM: vm}, err
|
|
}
|
|
|
|
func vmDeleteDispatch(ctx context.Context, d *Daemon, p api.VMRefParams) (api.VMShowResult, error) {
|
|
vm, err := d.vm.DeleteVM(ctx, p.IDOrName)
|
|
return api.VMShowResult{VM: vm}, err
|
|
}
|
|
|
|
func vmSetDispatch(ctx context.Context, d *Daemon, p api.VMSetParams) (api.VMShowResult, error) {
|
|
vm, err := d.vm.SetVM(ctx, p)
|
|
return api.VMShowResult{VM: vm}, err
|
|
}
|
|
|
|
func vmStatsDispatch(ctx context.Context, d *Daemon, p api.VMRefParams) (api.VMStatsResult, error) {
|
|
vm, stats, err := d.stats.GetVMStats(ctx, p.IDOrName)
|
|
return api.VMStatsResult{VM: vm, Stats: stats}, err
|
|
}
|
|
|
|
func vmHealthDispatch(ctx context.Context, d *Daemon, p api.VMRefParams) (api.VMHealthResult, error) {
|
|
return d.stats.HealthVM(ctx, p.IDOrName)
|
|
}
|
|
|
|
func vmPingDispatch(ctx context.Context, d *Daemon, p api.VMRefParams) (api.VMPingResult, error) {
|
|
return d.stats.PingVM(ctx, p.IDOrName)
|
|
}
|
|
|
|
func vmPortsDispatch(ctx context.Context, d *Daemon, p api.VMRefParams) (api.VMPortsResult, error) {
|
|
return d.stats.PortsVM(ctx, p.IDOrName)
|
|
}
|
|
|
|
func workspacePrepareDispatch(ctx context.Context, d *Daemon, p api.VMWorkspacePrepareParams) (api.VMWorkspacePrepareResult, error) {
|
|
ws, err := d.ws.PrepareVMWorkspace(ctx, p)
|
|
return api.VMWorkspacePrepareResult{Workspace: ws}, err
|
|
}
|
|
|
|
func workspaceExportDispatch(ctx context.Context, d *Daemon, p api.WorkspaceExportParams) (api.WorkspaceExportResult, error) {
|
|
return d.ws.ExportVMWorkspace(ctx, p)
|
|
}
|
|
|
|
func imageListDispatch(ctx context.Context, d *Daemon) (api.ImageListResult, error) {
|
|
images, err := d.store.ListImages(ctx)
|
|
return api.ImageListResult{Images: images}, err
|
|
}
|
|
|
|
func imageShowDispatch(ctx context.Context, d *Daemon, p api.ImageRefParams) (api.ImageShowResult, error) {
|
|
image, err := d.img.FindImage(ctx, p.IDOrName)
|
|
return api.ImageShowResult{Image: image}, err
|
|
}
|
|
|
|
func imageRegisterDispatch(ctx context.Context, d *Daemon, p api.ImageRegisterParams) (api.ImageShowResult, error) {
|
|
image, err := d.img.RegisterImage(ctx, p)
|
|
return api.ImageShowResult{Image: image}, err
|
|
}
|
|
|
|
func imagePromoteDispatch(ctx context.Context, d *Daemon, p api.ImageRefParams) (api.ImageShowResult, error) {
|
|
image, err := d.img.PromoteImage(ctx, p.IDOrName)
|
|
return api.ImageShowResult{Image: image}, err
|
|
}
|
|
|
|
func imageDeleteDispatch(ctx context.Context, d *Daemon, p api.ImageRefParams) (api.ImageShowResult, error) {
|
|
image, err := d.img.DeleteImage(ctx, p.IDOrName)
|
|
return api.ImageShowResult{Image: image}, err
|
|
}
|
|
|
|
func imagePullDispatch(ctx context.Context, d *Daemon, p api.ImagePullParams) (api.ImageShowResult, error) {
|
|
image, err := d.img.PullImage(ctx, p)
|
|
return api.ImageShowResult{Image: image}, err
|
|
}
|
|
|
|
func imageCachePruneDispatch(ctx context.Context, d *Daemon, p api.ImageCachePruneParams) (api.ImageCachePruneResult, error) {
|
|
return d.img.PruneOCICache(ctx, p)
|
|
}
|
|
|
|
func daemonOperationsListDispatch(ctx context.Context, d *Daemon) (api.OperationsListResult, error) {
|
|
return d.ListOperations(ctx)
|
|
}
|
|
|
|
func kernelListDispatch(ctx context.Context, d *Daemon) (api.KernelListResult, error) {
|
|
return d.img.KernelList(ctx)
|
|
}
|
|
|
|
func kernelShowDispatch(ctx context.Context, d *Daemon, p api.KernelRefParams) (api.KernelShowResult, error) {
|
|
entry, err := d.img.KernelShow(ctx, p.Name)
|
|
return api.KernelShowResult{Entry: entry}, err
|
|
}
|
|
|
|
func kernelDeleteDispatch(ctx context.Context, d *Daemon, p api.KernelRefParams) (api.Empty, error) {
|
|
return api.Empty{}, d.img.KernelDelete(ctx, p.Name)
|
|
}
|
|
|
|
func kernelImportDispatch(ctx context.Context, d *Daemon, p api.KernelImportParams) (api.KernelShowResult, error) {
|
|
entry, err := d.img.KernelImport(ctx, p)
|
|
return api.KernelShowResult{Entry: entry}, err
|
|
}
|
|
|
|
func kernelPullDispatch(ctx context.Context, d *Daemon, p api.KernelPullParams) (api.KernelShowResult, error) {
|
|
entry, err := d.img.KernelPull(ctx, p)
|
|
return api.KernelShowResult{Entry: entry}, err
|
|
}
|
|
|
|
func kernelCatalogDispatch(ctx context.Context, d *Daemon) (api.KernelCatalogResult, error) {
|
|
return d.img.KernelCatalog(ctx)
|
|
}
|
|
|
|
// ---- Special-case handlers: pre-service validation, custom error
|
|
// ---- codes, or raw rpc.NewResult encoding — things the generic
|
|
// ---- wrapper can't express.
|
|
|
|
// pingHandler is info-only: no service call, just a snapshot of
|
|
// build metadata. Raw rpc.NewResult to match the pre-refactor
|
|
// encoding; marshalResultOrError would over-wrap this.
|
|
func pingHandler(_ context.Context, d *Daemon, _ rpc.Request) rpc.Response {
|
|
info := buildinfo.Current()
|
|
result, _ := rpc.NewResult(api.PingResult{
|
|
Status: "ok",
|
|
PID: d.pid,
|
|
Version: info.Version,
|
|
Commit: info.Commit,
|
|
BuiltAt: info.BuiltAt,
|
|
})
|
|
return result
|
|
}
|
|
|
|
// shutdownHandler triggers async daemon shutdown. `d.Close` runs in
|
|
// a goroutine so the RPC response reaches the client before the
|
|
// listener closes.
|
|
func shutdownHandler(_ context.Context, d *Daemon, _ rpc.Request) rpc.Response {
|
|
go d.Close()
|
|
result, _ := rpc.NewResult(api.ShutdownResult{Status: "stopping"})
|
|
return result
|
|
}
|
|
|
|
// vmLogsHandler needs the "not_found" error code (distinct from
|
|
// "operation_failed") when FindVM misses, so the CLI can print a
|
|
// cleaner message. The generic paramHandler maps every service err
|
|
// to "operation_failed".
|
|
func vmLogsHandler(ctx context.Context, d *Daemon, req rpc.Request) rpc.Response {
|
|
params, err := rpc.DecodeParams[api.VMRefParams](req)
|
|
if err != nil {
|
|
return rpc.NewError("bad_request", err.Error())
|
|
}
|
|
vm, err := d.vm.FindVM(ctx, params.IDOrName)
|
|
if err != nil {
|
|
return rpc.NewError("not_found", err.Error())
|
|
}
|
|
return marshalResultOrError(api.VMLogsResult{LogPath: vm.Runtime.LogPath}, nil)
|
|
}
|
|
|
|
// vmSSHHandler does two pre-service validations: FindVM / TouchVM
|
|
// for "not_found", then vmAlive for "not_running". Both distinct
|
|
// error codes feed cleaner CLI output.
|
|
func vmSSHHandler(ctx context.Context, d *Daemon, req rpc.Request) rpc.Response {
|
|
params, err := rpc.DecodeParams[api.VMRefParams](req)
|
|
if err != nil {
|
|
return rpc.NewError("bad_request", err.Error())
|
|
}
|
|
vm, err := d.vm.TouchVM(ctx, params.IDOrName)
|
|
if err != nil {
|
|
return rpc.NewError("not_found", err.Error())
|
|
}
|
|
if !d.vm.vmAlive(vm) {
|
|
return rpc.NewError("not_running", fmt.Sprintf("vm %s is not running", vm.Name))
|
|
}
|
|
return marshalResultOrError(api.VMSSHResult{Name: vm.Name, GuestIP: vm.Runtime.GuestIP}, nil)
|
|
}
|