Serve a local web UI from bangerd
Add a localhost-only web console so VM and image management no longer depends on the CLI for every inspection and lifecycle action. Wire bangerd up to a configurable web listener, expose dashboard and async image-build state through the daemon, and serve CSRF-protected HTML pages with host-path picking, VM/image detail views, logs, ports, and progress polling for long-running operations. Keep the browser path aligned with the existing sudo and host-owned artifact model: surface sudo readiness, print the web URL in daemon status, and document the new workflow. Polish the UI with resource usage cards, clearer clickable affordances, cancel paths, confirmation prompts, image-name links, and HTTP port links. Validation: GOCACHE=/tmp/banger-gocache go test ./...
This commit is contained in:
parent
30f0c0b54a
commit
2362d0ae39
24 changed files with 3308 additions and 52 deletions
|
|
@ -9,6 +9,7 @@ import (
|
|||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
|
@ -26,27 +27,32 @@ import (
|
|||
)
|
||||
|
||||
type Daemon struct {
|
||||
layout paths.Layout
|
||||
config model.DaemonConfig
|
||||
store *store.Store
|
||||
runner system.CommandRunner
|
||||
logger *slog.Logger
|
||||
mu sync.Mutex
|
||||
createOpsMu sync.Mutex
|
||||
createOps map[string]*vmCreateOperationState
|
||||
vmLocksMu sync.Mutex
|
||||
vmLocks map[string]*sync.Mutex
|
||||
tapPoolMu sync.Mutex
|
||||
tapPool []string
|
||||
tapPoolNext int
|
||||
closing chan struct{}
|
||||
once sync.Once
|
||||
pid int
|
||||
listener net.Listener
|
||||
vmDNS *vmdns.Server
|
||||
vmCaps []vmCapability
|
||||
imageBuild func(context.Context, imageBuildSpec) error
|
||||
requestHandler func(context.Context, rpc.Request) rpc.Response
|
||||
layout paths.Layout
|
||||
config model.DaemonConfig
|
||||
store *store.Store
|
||||
runner system.CommandRunner
|
||||
logger *slog.Logger
|
||||
mu sync.Mutex
|
||||
createOpsMu sync.Mutex
|
||||
createOps map[string]*vmCreateOperationState
|
||||
imageBuildOpsMu sync.Mutex
|
||||
imageBuildOps map[string]*imageBuildOperationState
|
||||
vmLocksMu sync.Mutex
|
||||
vmLocks map[string]*sync.Mutex
|
||||
tapPoolMu sync.Mutex
|
||||
tapPool []string
|
||||
tapPoolNext int
|
||||
closing chan struct{}
|
||||
once sync.Once
|
||||
pid int
|
||||
listener net.Listener
|
||||
webListener net.Listener
|
||||
webServer *http.Server
|
||||
webURL string
|
||||
vmDNS *vmdns.Server
|
||||
vmCaps []vmCapability
|
||||
imageBuild func(context.Context, imageBuildSpec) error
|
||||
requestHandler func(context.Context, rpc.Request) rpc.Response
|
||||
}
|
||||
|
||||
func Open(ctx context.Context) (d *Daemon, err error) {
|
||||
|
|
@ -115,6 +121,12 @@ func (d *Daemon) Close() error {
|
|||
if d.listener != nil {
|
||||
_ = d.listener.Close()
|
||||
}
|
||||
if d.webServer != nil {
|
||||
_ = d.webServer.Close()
|
||||
}
|
||||
if d.webListener != nil {
|
||||
_ = d.webListener.Close()
|
||||
}
|
||||
err = errors.Join(d.stopVMDNS(), d.store.Close())
|
||||
})
|
||||
return err
|
||||
|
|
@ -138,6 +150,9 @@ func (d *Daemon) Serve(ctx context.Context) error {
|
|||
if d.logger != nil {
|
||||
d.logger.Info("daemon serving", "socket", d.layout.SocketPath, "pid", d.pid)
|
||||
}
|
||||
if err := d.startWebServer(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go d.backgroundLoop()
|
||||
|
||||
|
|
@ -238,7 +253,7 @@ func (d *Daemon) dispatch(ctx context.Context, req rpc.Request) rpc.Response {
|
|||
}
|
||||
switch req.Method {
|
||||
case "ping":
|
||||
result, _ := rpc.NewResult(api.PingResult{Status: "ok", PID: d.pid})
|
||||
result, _ := rpc.NewResult(api.PingResult{Status: "ok", PID: d.pid, WebURL: d.webURL})
|
||||
return result
|
||||
case "shutdown":
|
||||
go d.Close()
|
||||
|
|
@ -392,6 +407,27 @@ func (d *Daemon) dispatch(ctx context.Context, req rpc.Request) rpc.Response {
|
|||
}
|
||||
image, err := d.BuildImage(ctx, params)
|
||||
return marshalResultOrError(api.ImageShowResult{Image: image}, err)
|
||||
case "image.build.begin":
|
||||
params, err := rpc.DecodeParams[api.ImageBuildParams](req)
|
||||
if err != nil {
|
||||
return rpc.NewError("bad_request", err.Error())
|
||||
}
|
||||
op, err := d.BeginImageBuild(ctx, params)
|
||||
return marshalResultOrError(api.ImageBuildBeginResult{Operation: op}, err)
|
||||
case "image.build.status":
|
||||
params, err := rpc.DecodeParams[api.ImageBuildStatusParams](req)
|
||||
if err != nil {
|
||||
return rpc.NewError("bad_request", err.Error())
|
||||
}
|
||||
op, err := d.ImageBuildStatus(ctx, params.ID)
|
||||
return marshalResultOrError(api.ImageBuildStatusResult{Operation: op}, err)
|
||||
case "image.build.cancel":
|
||||
params, err := rpc.DecodeParams[api.ImageBuildStatusParams](req)
|
||||
if err != nil {
|
||||
return rpc.NewError("bad_request", err.Error())
|
||||
}
|
||||
err = d.CancelImageBuild(ctx, params.ID)
|
||||
return marshalResultOrError(api.Empty{}, err)
|
||||
case "image.register":
|
||||
params, err := rpc.DecodeParams[api.ImageRegisterParams](req)
|
||||
if err != nil {
|
||||
|
|
@ -436,6 +472,7 @@ func (d *Daemon) backgroundLoop() {
|
|||
d.logger.Error("background stale sweep failed", "error", err.Error())
|
||||
}
|
||||
d.pruneVMCreateOperations(time.Now().Add(-10 * time.Minute))
|
||||
d.pruneImageBuildOperations(time.Now().Add(-10 * time.Minute))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue