diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index 6ef0375..daf0d70 100644 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -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() { diff --git a/internal/daemon/dispatch.go b/internal/daemon/dispatch.go new file mode 100644 index 0000000..5fd5c3d --- /dev/null +++ b/internal/daemon/dispatch.go @@ -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) +} diff --git a/internal/daemon/dispatch_test.go b/internal/daemon/dispatch_test.go new file mode 100644 index 0000000..18e7bd8 --- /dev/null +++ b/internal/daemon/dispatch_test.go @@ -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) + } + } +}