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:
+76
-14
@@ -21,23 +21,39 @@ import (
|
||||
)
|
||||
|
||||
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 45*time.Second)
|
||||
defer cancel()
|
||||
|
||||
body := map[string]any{
|
||||
"ok": true,
|
||||
"dockerHost": s.docker.Endpoint,
|
||||
"packageDir": s.cfg.PackageDir,
|
||||
"dataDir": s.cfg.DataDir,
|
||||
"knownHosts": s.hosts.Path(),
|
||||
"authRequired": s.cfg.Token != "",
|
||||
}
|
||||
if v, err := s.docker.Ping(ctx); err != nil {
|
||||
|
||||
// Health is also what connects to the selected source on a fresh start, so
|
||||
// the UI learns straight away when the remembered source is unreachable.
|
||||
conn, release, err := s.source(ctx)
|
||||
if err != nil {
|
||||
selected, _ := s.sources.Get(s.sources.Selected())
|
||||
body["ok"] = false
|
||||
body["dockerError"] = err.Error()
|
||||
} else {
|
||||
body["dockerVersion"] = v
|
||||
endpoint := sourceEndpoint(selected)
|
||||
body["dockerHost"] = endpoint
|
||||
body["source"] = sourceStatus{
|
||||
ID: selected.ID, Name: selected.Name, Kind: selected.Kind,
|
||||
Endpoint: endpoint, Error: err.Error(),
|
||||
}
|
||||
writeJSON(w, http.StatusOK, body)
|
||||
return
|
||||
}
|
||||
defer release()
|
||||
|
||||
st := conn.status()
|
||||
body["dockerHost"] = st.Endpoint
|
||||
body["dockerVersion"] = st.DockerVersion
|
||||
body["source"] = st
|
||||
writeJSON(w, http.StatusOK, body)
|
||||
}
|
||||
|
||||
@@ -47,7 +63,14 @@ func (s *Server) handleSource(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
inv, err := s.docker.Inventory(ctx)
|
||||
conn, release, err := s.source(ctx)
|
||||
if err != nil {
|
||||
s.writeDialError(w, err)
|
||||
return
|
||||
}
|
||||
defer release()
|
||||
|
||||
inv, err := conn.docker.Inventory(ctx)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadGateway, "%v", err)
|
||||
return
|
||||
@@ -69,7 +92,14 @@ func (s *Server) handleSourceSizes(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
sizes, err := s.docker.VolumeSizes(ctx)
|
||||
conn, release, err := s.source(ctx)
|
||||
if err != nil {
|
||||
s.writeDialError(w, err)
|
||||
return
|
||||
}
|
||||
defer release()
|
||||
|
||||
sizes, err := conn.docker.VolumeSizes(ctx)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadGateway, "%v", err)
|
||||
return
|
||||
@@ -209,7 +239,14 @@ func (s *Server) handlePreview(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
inv, err := s.docker.Inventory(ctx)
|
||||
conn, release, err := s.source(ctx)
|
||||
if err != nil {
|
||||
s.writeDialError(w, err)
|
||||
return
|
||||
}
|
||||
defer release()
|
||||
|
||||
inv, err := conn.docker.Inventory(ctx)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadGateway, "%v", err)
|
||||
return
|
||||
@@ -299,12 +336,23 @@ func (s *Server) handleMigrateSSH(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// The source is held for the whole job: picking another source in the UI
|
||||
// while this runs must not close the socket it is reading from.
|
||||
srcCtx, srcCancel := context.WithTimeout(r.Context(), 60*time.Second)
|
||||
src, release, err := s.source(srcCtx)
|
||||
srcCancel()
|
||||
if err != nil {
|
||||
s.writeDialError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// The inventory is re-read now so the plan is applied to current state
|
||||
// rather than to whatever the browser last loaded.
|
||||
invCtx, cancel := context.WithTimeout(r.Context(), 2*time.Minute)
|
||||
inv, err := s.docker.Inventory(invCtx)
|
||||
inv, err := src.docker.Inventory(invCtx)
|
||||
cancel()
|
||||
if err != nil {
|
||||
release()
|
||||
writeError(w, http.StatusBadGateway, "%v", err)
|
||||
return
|
||||
}
|
||||
@@ -315,13 +363,14 @@ func (s *Server) handleMigrateSSH(w http.ResponseWriter, r *http.Request) {
|
||||
client, err := sshx.Dial(dialCtx, cfg, s.hosts)
|
||||
dialCancel()
|
||||
if err != nil {
|
||||
release()
|
||||
s.writeDialError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
title := fmt.Sprintf("%d container(s) to %s", countIncluded(req.Plan), cfg.Name)
|
||||
title := fmt.Sprintf("%d container(s) from %s to %s", countIncluded(req.Plan), src.src.Name, cfg.Name)
|
||||
runner := &migrate.SSHRunner{
|
||||
Src: s.docker,
|
||||
Src: src.docker,
|
||||
Dst: sshx.NewRemoteDocker(client),
|
||||
Containers: inv.Containers,
|
||||
Volumes: inv.Volumes,
|
||||
@@ -331,8 +380,10 @@ func (s *Server) handleMigrateSSH(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
j := s.jobs.Run(context.Background(), job.KindSSH, title, req.Plan.Options.DryRun,
|
||||
func(ctx context.Context, j *job.Job) error {
|
||||
defer release()
|
||||
defer client.Close()
|
||||
j.Logf(job.LevelInfo, "", "migrating to %s@%s over ssh", cfg.User, cfg.Host)
|
||||
j.Logf(job.LevelInfo, "", "migrating from %s to %s@%s over ssh",
|
||||
src.docker.Endpoint, cfg.User, cfg.Host)
|
||||
return runner.Run(ctx, j)
|
||||
})
|
||||
|
||||
@@ -362,16 +413,25 @@ func (s *Server) handleBuildPackage(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
srcCtx, srcCancel := context.WithTimeout(r.Context(), 60*time.Second)
|
||||
src, release, err := s.source(srcCtx)
|
||||
srcCancel()
|
||||
if err != nil {
|
||||
s.writeDialError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
invCtx, cancel := context.WithTimeout(r.Context(), 2*time.Minute)
|
||||
inv, err := s.docker.Inventory(invCtx)
|
||||
inv, err := src.docker.Inventory(invCtx)
|
||||
cancel()
|
||||
if err != nil {
|
||||
release()
|
||||
writeError(w, http.StatusBadGateway, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
packager := &migrate.Packager{
|
||||
Src: s.docker,
|
||||
Src: src.docker,
|
||||
Containers: inv.Containers,
|
||||
Volumes: inv.Volumes,
|
||||
Networks: inv.Networks,
|
||||
@@ -384,6 +444,8 @@ func (s *Server) handleBuildPackage(w http.ResponseWriter, r *http.Request) {
|
||||
title := fmt.Sprintf("package of %d container(s)", countIncluded(req.Plan))
|
||||
j := s.jobs.Run(context.Background(), job.KindPackage, title, req.Plan.Options.DryRun,
|
||||
func(ctx context.Context, j *job.Job) error {
|
||||
defer release()
|
||||
j.Logf(job.LevelInfo, "", "reading from %s", src.docker.Endpoint)
|
||||
res, err := packager.Run(ctx, j)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user