Add SSH source support with dialstdio and UI components

- Implement SSH dial via stdio for remote connections
- Add sources API and storage layer for managing connection sources
- Add SourcePanel and SshFields web components for SSH configuration
- Update app structure to support source-based connections
- Update handlers and server for new sources endpoint

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-08-13 10:19:47 +02:00
co-authored by Claude Haiku 4.5
parent 05db8bfeb9
commit 9b354636bb
19 changed files with 1857 additions and 174 deletions
+46 -17
View File
@@ -11,9 +11,9 @@ import (
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/arescom/dockmv/internal/dkr"
"github.com/arescom/dockmv/internal/job"
"github.com/arescom/dockmv/internal/sshx"
"github.com/arescom/dockmv/internal/store"
@@ -29,23 +29,31 @@ type Config struct {
DataDir string
// PackageDir is where migration packages are written.
PackageDir string
// DockerHost overrides the source daemon address.
// DockerHost overrides the local source daemon address.
DockerHost string
// DockerHostSet reports that DockerHost was given explicitly on the command
// line. It then wins over the source remembered from the last run.
DockerHostSet bool
// UI is the embedded web app; nil disables the UI.
UI fs.FS
// Logger receives request and error logs.
Logger *slog.Logger
}
// Server ties the Docker client, connection store and job manager to HTTP.
// Server ties the source daemon, connection store and job manager to HTTP.
type Server struct {
cfg Config
log *slog.Logger
docker *dkr.Client
conns *store.Connections
hosts *sshx.KnownHosts
jobs *job.Manager
mux *http.ServeMux
cfg Config
log *slog.Logger
sources *store.Sources
conns *store.Connections
hosts *sshx.KnownHosts
jobs *job.Manager
mux *http.ServeMux
// srcMu guards cur, the connection to the selected source. It is opened on
// first use and replaced when the operator picks another source.
srcMu sync.Mutex
cur *sourceConn
}
// New builds the server and everything it owns.
@@ -60,10 +68,6 @@ func New(cfg Config) (*Server, error) {
return nil, fmt.Errorf("create package directory: %w", err)
}
docker, err := dkr.New(cfg.DockerHost)
if err != nil {
return nil, err
}
conns, err := store.NewConnections(filepath.Join(cfg.DataDir, "connections.json"))
if err != nil {
return nil, err
@@ -72,17 +76,35 @@ func New(cfg Config) (*Server, error) {
if err != nil {
return nil, err
}
local := store.Source{Name: "this host", DockerHost: cfg.DockerHost}
if local.DockerHost == "" {
local.DockerHost = os.Getenv("DOCKER_HOST")
}
sources, err := store.NewSources(filepath.Join(cfg.DataDir, "sources.json"), local)
if err != nil {
return nil, err
}
// An explicit --docker-host is an instruction for this run, so it overrides
// the source remembered from the last one.
if cfg.DockerHostSet {
if err := sources.Select(store.LocalSourceID); err != nil {
return nil, err
}
}
s := &Server{
cfg: cfg, log: cfg.Logger, docker: docker,
cfg: cfg, log: cfg.Logger, sources: sources,
conns: conns, hosts: hosts, jobs: job.NewManager(), mux: http.NewServeMux(),
}
s.routes()
return s, nil
}
// Close releases the Docker connection.
func (s *Server) Close() error { return s.docker.Close() }
// Close releases the connection to the current source.
func (s *Server) Close() error {
s.invalidateSource("")
return nil
}
// Handler returns the root HTTP handler.
func (s *Server) Handler() http.Handler {
@@ -96,6 +118,13 @@ func (s *Server) routes() {
m.HandleFunc("GET /api/source", s.handleSource)
m.HandleFunc("GET /api/source/sizes", s.handleSourceSizes)
m.HandleFunc("GET /api/sources", s.handleListSources)
m.HandleFunc("POST /api/sources", s.handleSaveSource)
m.HandleFunc("DELETE /api/sources/{id}", s.handleDeleteSource)
m.HandleFunc("POST /api/sources/{id}/select", s.handleSelectSource)
m.HandleFunc("POST /api/sources/{id}/probe", s.handleSourceProbe)
m.HandleFunc("POST /api/sources/{id}/trust", s.handleSourceTrust)
m.HandleFunc("GET /api/connections", s.handleListConnections)
m.HandleFunc("POST /api/connections", s.handleSaveConnection)
m.HandleFunc("DELETE /api/connections/{id}", s.handleDeleteConnection)