daemon: replace RPC switch with generic method-to-handler table
The dispatch method was a single ~240-line switch of 34 cases, each
following the same pattern: decode params into some type P, call a
service method returning (R, error), wrap R in a result struct and
either marshalResultOrError-encode or return a raw rpc.NewError.
Adding a method was a 4-line ceremony per site, and grepping for
"methods banger speaks" meant reading the full switch.
New shape, in internal/daemon/dispatch.go:
- handler is the uniform `func(ctx, d, req) rpc.Response` type
every method dispatches through.
- paramHandler[P, R] is the generic wrapper that absorbs 28 of
the 34 cases (decode, call, marshal). No reflection — P and R
are deduced from the service-call literal, so each map entry
is a one-liner referencing a small adapter func.
- noParamHandler[R] is the decode-free variant for 6 methods
that don't carry params.
- rpcHandlers is the single source of truth for which methods
exist and which adapter they dispatch to.
- Four specials (ping, shutdown, vm.logs, vm.ssh) stay as named
`handler`-typed functions: ping/shutdown encode with raw
rpc.NewResult, vm.logs/vm.ssh need pre-service validation to
emit distinct error codes (not_found, not_running) that the
generic wrapper maps uniformly to operation_failed.
Daemon.dispatch shrinks from a 240-line switch to 11 lines:
version check, test-only handler short-circuit, table lookup,
invoke-or-unknown.
Tests:
- TestRPCHandlersMatchDocumentedMethods — keyset guard. Adding
or removing a method without updating the expected slice is a
red flag the test surfaces.
- TestRPCHandlersAllNonNil — catches nil-function registrations.
All pre-existing dispatch tests (param decode, error codes, etc.)
keep passing unchanged — the handler contract for any given
method is byte-identical from the RPC client's perspective. Smoke
(all 21 scenarios) exercises every code path end-to-end.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
11a33604c0
commit
366e1560c9
3 changed files with 386 additions and 233 deletions
|
|
@ -12,8 +12,6 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"banger/internal/api"
|
||||
"banger/internal/buildinfo"
|
||||
"banger/internal/config"
|
||||
ws "banger/internal/daemon/workspace"
|
||||
"banger/internal/model"
|
||||
|
|
@ -261,239 +259,11 @@ func (d *Daemon) dispatch(ctx context.Context, req rpc.Request) rpc.Response {
|
|||
if d.requestHandler != nil {
|
||||
return d.requestHandler(ctx, req)
|
||||
}
|
||||
switch req.Method {
|
||||
case "ping":
|
||||
info := buildinfo.Current()
|
||||
result, _ := rpc.NewResult(api.PingResult{
|
||||
Status: "ok",
|
||||
PID: d.pid,
|
||||
Version: info.Version,
|
||||
Commit: info.Commit,
|
||||
BuiltAt: info.BuiltAt,
|
||||
})
|
||||
return result
|
||||
case "shutdown":
|
||||
go d.Close()
|
||||
result, _ := rpc.NewResult(api.ShutdownResult{Status: "stopping"})
|
||||
return result
|
||||
case "vm.create":
|
||||
params, err := rpc.DecodeParams[api.VMCreateParams](req)
|
||||
if err != nil {
|
||||
return rpc.NewError("bad_request", err.Error())
|
||||
}
|
||||
vm, err := d.vm.CreateVM(ctx, params)
|
||||
return marshalResultOrError(api.VMShowResult{VM: vm}, err)
|
||||
case "vm.create.begin":
|
||||
params, err := rpc.DecodeParams[api.VMCreateParams](req)
|
||||
if err != nil {
|
||||
return rpc.NewError("bad_request", err.Error())
|
||||
}
|
||||
op, err := d.vm.BeginVMCreate(ctx, params)
|
||||
return marshalResultOrError(api.VMCreateBeginResult{Operation: op}, err)
|
||||
case "vm.create.status":
|
||||
params, err := rpc.DecodeParams[api.VMCreateStatusParams](req)
|
||||
if err != nil {
|
||||
return rpc.NewError("bad_request", err.Error())
|
||||
}
|
||||
op, err := d.vm.VMCreateStatus(ctx, params.ID)
|
||||
return marshalResultOrError(api.VMCreateStatusResult{Operation: op}, err)
|
||||
case "vm.create.cancel":
|
||||
params, err := rpc.DecodeParams[api.VMCreateStatusParams](req)
|
||||
if err != nil {
|
||||
return rpc.NewError("bad_request", err.Error())
|
||||
}
|
||||
err = d.vm.CancelVMCreate(ctx, params.ID)
|
||||
return marshalResultOrError(api.Empty{}, err)
|
||||
case "vm.list":
|
||||
vms, err := d.store.ListVMs(ctx)
|
||||
return marshalResultOrError(api.VMListResult{VMs: vms}, err)
|
||||
case "vm.show":
|
||||
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)
|
||||
return marshalResultOrError(api.VMShowResult{VM: vm}, err)
|
||||
case "vm.start":
|
||||
params, err := rpc.DecodeParams[api.VMRefParams](req)
|
||||
if err != nil {
|
||||
return rpc.NewError("bad_request", err.Error())
|
||||
}
|
||||
vm, err := d.vm.StartVM(ctx, params.IDOrName)
|
||||
return marshalResultOrError(api.VMShowResult{VM: vm}, err)
|
||||
case "vm.stop":
|
||||
params, err := rpc.DecodeParams[api.VMRefParams](req)
|
||||
if err != nil {
|
||||
return rpc.NewError("bad_request", err.Error())
|
||||
}
|
||||
vm, err := d.vm.StopVM(ctx, params.IDOrName)
|
||||
return marshalResultOrError(api.VMShowResult{VM: vm}, err)
|
||||
case "vm.kill":
|
||||
params, err := rpc.DecodeParams[api.VMKillParams](req)
|
||||
if err != nil {
|
||||
return rpc.NewError("bad_request", err.Error())
|
||||
}
|
||||
vm, err := d.vm.KillVM(ctx, params)
|
||||
return marshalResultOrError(api.VMShowResult{VM: vm}, err)
|
||||
case "vm.restart":
|
||||
params, err := rpc.DecodeParams[api.VMRefParams](req)
|
||||
if err != nil {
|
||||
return rpc.NewError("bad_request", err.Error())
|
||||
}
|
||||
vm, err := d.vm.RestartVM(ctx, params.IDOrName)
|
||||
return marshalResultOrError(api.VMShowResult{VM: vm}, err)
|
||||
case "vm.delete":
|
||||
params, err := rpc.DecodeParams[api.VMRefParams](req)
|
||||
if err != nil {
|
||||
return rpc.NewError("bad_request", err.Error())
|
||||
}
|
||||
vm, err := d.vm.DeleteVM(ctx, params.IDOrName)
|
||||
return marshalResultOrError(api.VMShowResult{VM: vm}, err)
|
||||
case "vm.set":
|
||||
params, err := rpc.DecodeParams[api.VMSetParams](req)
|
||||
if err != nil {
|
||||
return rpc.NewError("bad_request", err.Error())
|
||||
}
|
||||
vm, err := d.vm.SetVM(ctx, params)
|
||||
return marshalResultOrError(api.VMShowResult{VM: vm}, err)
|
||||
case "vm.stats":
|
||||
params, err := rpc.DecodeParams[api.VMRefParams](req)
|
||||
if err != nil {
|
||||
return rpc.NewError("bad_request", err.Error())
|
||||
}
|
||||
vm, stats, err := d.vm.GetVMStats(ctx, params.IDOrName)
|
||||
return marshalResultOrError(api.VMStatsResult{VM: vm, Stats: stats}, err)
|
||||
case "vm.logs":
|
||||
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)
|
||||
case "vm.ssh":
|
||||
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)
|
||||
case "vm.health":
|
||||
params, err := rpc.DecodeParams[api.VMRefParams](req)
|
||||
if err != nil {
|
||||
return rpc.NewError("bad_request", err.Error())
|
||||
}
|
||||
result, err := d.vm.HealthVM(ctx, params.IDOrName)
|
||||
return marshalResultOrError(result, err)
|
||||
case "vm.ping":
|
||||
params, err := rpc.DecodeParams[api.VMRefParams](req)
|
||||
if err != nil {
|
||||
return rpc.NewError("bad_request", err.Error())
|
||||
}
|
||||
result, err := d.vm.PingVM(ctx, params.IDOrName)
|
||||
return marshalResultOrError(result, err)
|
||||
case "vm.ports":
|
||||
params, err := rpc.DecodeParams[api.VMRefParams](req)
|
||||
if err != nil {
|
||||
return rpc.NewError("bad_request", err.Error())
|
||||
}
|
||||
result, err := d.vm.PortsVM(ctx, params.IDOrName)
|
||||
return marshalResultOrError(result, err)
|
||||
case "vm.workspace.prepare":
|
||||
params, err := rpc.DecodeParams[api.VMWorkspacePrepareParams](req)
|
||||
if err != nil {
|
||||
return rpc.NewError("bad_request", err.Error())
|
||||
}
|
||||
workspace, err := d.ws.PrepareVMWorkspace(ctx, params)
|
||||
return marshalResultOrError(api.VMWorkspacePrepareResult{Workspace: workspace}, err)
|
||||
case "vm.workspace.export":
|
||||
params, err := rpc.DecodeParams[api.WorkspaceExportParams](req)
|
||||
if err != nil {
|
||||
return rpc.NewError("bad_request", err.Error())
|
||||
}
|
||||
result, err := d.ws.ExportVMWorkspace(ctx, params)
|
||||
return marshalResultOrError(result, err)
|
||||
case "image.list":
|
||||
images, err := d.store.ListImages(ctx)
|
||||
return marshalResultOrError(api.ImageListResult{Images: images}, err)
|
||||
case "image.show":
|
||||
params, err := rpc.DecodeParams[api.ImageRefParams](req)
|
||||
if err != nil {
|
||||
return rpc.NewError("bad_request", err.Error())
|
||||
}
|
||||
image, err := d.img.FindImage(ctx, params.IDOrName)
|
||||
return marshalResultOrError(api.ImageShowResult{Image: image}, err)
|
||||
case "image.register":
|
||||
params, err := rpc.DecodeParams[api.ImageRegisterParams](req)
|
||||
if err != nil {
|
||||
return rpc.NewError("bad_request", err.Error())
|
||||
}
|
||||
image, err := d.img.RegisterImage(ctx, params)
|
||||
return marshalResultOrError(api.ImageShowResult{Image: image}, err)
|
||||
case "image.promote":
|
||||
params, err := rpc.DecodeParams[api.ImageRefParams](req)
|
||||
if err != nil {
|
||||
return rpc.NewError("bad_request", err.Error())
|
||||
}
|
||||
image, err := d.img.PromoteImage(ctx, params.IDOrName)
|
||||
return marshalResultOrError(api.ImageShowResult{Image: image}, err)
|
||||
case "image.delete":
|
||||
params, err := rpc.DecodeParams[api.ImageRefParams](req)
|
||||
if err != nil {
|
||||
return rpc.NewError("bad_request", err.Error())
|
||||
}
|
||||
image, err := d.img.DeleteImage(ctx, params.IDOrName)
|
||||
return marshalResultOrError(api.ImageShowResult{Image: image}, err)
|
||||
case "image.pull":
|
||||
params, err := rpc.DecodeParams[api.ImagePullParams](req)
|
||||
if err != nil {
|
||||
return rpc.NewError("bad_request", err.Error())
|
||||
}
|
||||
image, err := d.img.PullImage(ctx, params)
|
||||
return marshalResultOrError(api.ImageShowResult{Image: image}, err)
|
||||
case "kernel.list":
|
||||
return marshalResultOrError(d.img.KernelList(ctx))
|
||||
case "kernel.show":
|
||||
params, err := rpc.DecodeParams[api.KernelRefParams](req)
|
||||
if err != nil {
|
||||
return rpc.NewError("bad_request", err.Error())
|
||||
}
|
||||
entry, err := d.img.KernelShow(ctx, params.Name)
|
||||
return marshalResultOrError(api.KernelShowResult{Entry: entry}, err)
|
||||
case "kernel.delete":
|
||||
params, err := rpc.DecodeParams[api.KernelRefParams](req)
|
||||
if err != nil {
|
||||
return rpc.NewError("bad_request", err.Error())
|
||||
}
|
||||
err = d.img.KernelDelete(ctx, params.Name)
|
||||
return marshalResultOrError(api.Empty{}, err)
|
||||
case "kernel.import":
|
||||
params, err := rpc.DecodeParams[api.KernelImportParams](req)
|
||||
if err != nil {
|
||||
return rpc.NewError("bad_request", err.Error())
|
||||
}
|
||||
entry, err := d.img.KernelImport(ctx, params)
|
||||
return marshalResultOrError(api.KernelShowResult{Entry: entry}, err)
|
||||
case "kernel.pull":
|
||||
params, err := rpc.DecodeParams[api.KernelPullParams](req)
|
||||
if err != nil {
|
||||
return rpc.NewError("bad_request", err.Error())
|
||||
}
|
||||
entry, err := d.img.KernelPull(ctx, params)
|
||||
return marshalResultOrError(api.KernelShowResult{Entry: entry}, err)
|
||||
case "kernel.catalog":
|
||||
return marshalResultOrError(d.img.KernelCatalog(ctx))
|
||||
default:
|
||||
h, ok := rpcHandlers[req.Method]
|
||||
if !ok {
|
||||
return rpc.NewError("unknown_method", req.Method)
|
||||
}
|
||||
return h(ctx, d, req)
|
||||
}
|
||||
|
||||
func (d *Daemon) backgroundLoop() {
|
||||
|
|
|
|||
299
internal/daemon/dispatch.go
Normal file
299
internal/daemon/dispatch.go
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
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,
|
||||
|
||||
"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),
|
||||
|
||||
"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.vm.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.vm.HealthVM(ctx, p.IDOrName)
|
||||
}
|
||||
|
||||
func vmPingDispatch(ctx context.Context, d *Daemon, p api.VMRefParams) (api.VMPingResult, error) {
|
||||
return d.vm.PingVM(ctx, p.IDOrName)
|
||||
}
|
||||
|
||||
func vmPortsDispatch(ctx context.Context, d *Daemon, p api.VMRefParams) (api.VMPortsResult, error) {
|
||||
return d.vm.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 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)
|
||||
}
|
||||
84
internal/daemon/dispatch_test.go
Normal file
84
internal/daemon/dispatch_test.go
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
package daemon
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestRPCHandlersMatchDocumentedMethods pins the surface of the RPC
|
||||
// table: adding or removing a method should be an explicit, reviewable
|
||||
// change. If the keyset drifts and this test isn't updated alongside,
|
||||
// that's a red flag — either the documented list is stale, or a
|
||||
// method sneaked in without being discussed.
|
||||
//
|
||||
// The expected list is the single source of truth for "methods
|
||||
// banger speaks." Any production code consulting it (CLI completions,
|
||||
// docs generator) can grep this test.
|
||||
func TestRPCHandlersMatchDocumentedMethods(t *testing.T) {
|
||||
expected := []string{
|
||||
"image.delete",
|
||||
"image.list",
|
||||
"image.promote",
|
||||
"image.pull",
|
||||
"image.register",
|
||||
"image.show",
|
||||
|
||||
"kernel.catalog",
|
||||
"kernel.delete",
|
||||
"kernel.import",
|
||||
"kernel.list",
|
||||
"kernel.pull",
|
||||
"kernel.show",
|
||||
|
||||
"ping",
|
||||
"shutdown",
|
||||
|
||||
"vm.create",
|
||||
"vm.create.begin",
|
||||
"vm.create.cancel",
|
||||
"vm.create.status",
|
||||
"vm.delete",
|
||||
"vm.health",
|
||||
"vm.kill",
|
||||
"vm.list",
|
||||
"vm.logs",
|
||||
"vm.ping",
|
||||
"vm.ports",
|
||||
"vm.restart",
|
||||
"vm.set",
|
||||
"vm.show",
|
||||
"vm.ssh",
|
||||
"vm.start",
|
||||
"vm.stats",
|
||||
"vm.stop",
|
||||
|
||||
"vm.workspace.export",
|
||||
"vm.workspace.prepare",
|
||||
}
|
||||
|
||||
got := make([]string, 0, len(rpcHandlers))
|
||||
for name := range rpcHandlers {
|
||||
got = append(got, name)
|
||||
}
|
||||
sort.Strings(got)
|
||||
sort.Strings(expected)
|
||||
|
||||
if len(got) != len(expected) {
|
||||
t.Fatalf("method count: got %d, want %d\n got: %v\n want: %v", len(got), len(expected), got, expected)
|
||||
}
|
||||
for i := range expected {
|
||||
if got[i] != expected[i] {
|
||||
t.Fatalf("method[%d]: got %q, want %q\n full got: %v\n full want: %v", i, got[i], expected[i], got, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestRPCHandlersAllNonNil catches a silly-but-possible footgun:
|
||||
// registering a method with a nil function literal.
|
||||
func TestRPCHandlersAllNonNil(t *testing.T) {
|
||||
for name, h := range rpcHandlers {
|
||||
if h == nil {
|
||||
t.Errorf("rpcHandlers[%q] = nil", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue