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:
+4
-104
@@ -4,6 +4,7 @@ import type {
|
||||
Connection, HostKeyInfo, JobSnapshot, Options, Plan, PreviewResponse, SourceResponse, TargetInventory,
|
||||
} from './types'
|
||||
import { Check, Field, humanBytes, Modal, Notice } from './ui'
|
||||
import { HostKeyDialog, SshFields } from './SshFields'
|
||||
|
||||
export function Sidebar({
|
||||
source, plan, includedCount, options, setOptions,
|
||||
@@ -302,37 +303,7 @@ export function Sidebar({
|
||||
)}
|
||||
|
||||
{hostKey && (
|
||||
<Modal
|
||||
title="SSH host key"
|
||||
onClose={() => setHostKey(null)}
|
||||
footer={
|
||||
<>
|
||||
<button className="btn" onClick={() => setHostKey(null)}>cancel</button>
|
||||
<button className="btn primary" onClick={trustHostKey}>
|
||||
{hostKey.changed ? 'replace the stored key and trust' : 'trust this host'}
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="stack">
|
||||
{hostKey.changed && (
|
||||
<Notice kind="err">
|
||||
The key presented by this host is <b>different</b> from the one recorded earlier. This happens after a
|
||||
reinstall — but it is also what a machine-in-the-middle looks like. Only continue if you know why it
|
||||
changed.
|
||||
</Notice>
|
||||
)}
|
||||
{hostKey.trusted && !hostKey.changed && <Notice kind="ok">This host key is already trusted.</Notice>}
|
||||
<div className="small muted">
|
||||
Compare this with the output of <span className="mono">ssh-keyscan -t {hostKey.keyType} {hostKey.host}</span>{' '}
|
||||
run on the target itself, or with <span className="mono">ssh-keygen -lf /etc/ssh/ssh_host_*_key.pub</span>.
|
||||
</div>
|
||||
<div className="fingerprint">
|
||||
{hostKey.keyType}<br />
|
||||
{hostKey.fingerprint}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<HostKeyDialog info={hostKey} onClose={() => setHostKey(null)} onTrust={trustHostKey} />
|
||||
)}
|
||||
|
||||
{preview && <PreviewModal data={preview} onClose={() => setPreview(null)} />}
|
||||
@@ -382,79 +353,8 @@ function ConnectionDialog({
|
||||
}
|
||||
>
|
||||
<div className="stack" style={{ gap: 12 }}>
|
||||
<div className="row" style={{ gap: 12 }}>
|
||||
<Field label="label"><input type="text" value={c.name ?? ''} onChange={(e) => set('name', e.target.value)} /></Field>
|
||||
<Field label="host"><input type="text" value={c.host ?? ''} onChange={(e) => set('host', e.target.value)} /></Field>
|
||||
<div style={{ width: 90 }}>
|
||||
<Field label="port">
|
||||
<input type="number" value={c.port ?? 22} onChange={(e) => set('port', Number(e.target.value))} />
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row" style={{ gap: 12 }}>
|
||||
<Field label="user"><input type="text" value={c.user ?? ''} onChange={(e) => set('user', e.target.value)} /></Field>
|
||||
<Field label="authentication">
|
||||
<select value={c.auth ?? 'password'} onChange={(e) => set('auth', e.target.value as Connection['auth'])}>
|
||||
<option value="password">password</option>
|
||||
<option value="key">private key</option>
|
||||
<option value="agent">ssh agent</option>
|
||||
</select>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
{c.auth === 'password' && (
|
||||
<Field label="password">
|
||||
<input type="password" value={c.password ?? ''} onChange={(e) => set('password', e.target.value)} />
|
||||
</Field>
|
||||
)}
|
||||
|
||||
{c.auth === 'key' && (
|
||||
<>
|
||||
<Field label="private key path on this machine (leave empty to paste the key below)">
|
||||
<input type="text" placeholder="/root/.ssh/id_ed25519" value={c.privateKeyPath ?? ''} onChange={(e) => set('privateKeyPath', e.target.value)} />
|
||||
</Field>
|
||||
<Field label="or paste the private key">
|
||||
<textarea rows={5} value={c.privateKey ?? ''} onChange={(e) => set('privateKey', e.target.value)} placeholder="-----BEGIN OPENSSH PRIVATE KEY-----" />
|
||||
</Field>
|
||||
<Field label="passphrase (if the key is encrypted)">
|
||||
<input type="password" value={c.passphrase ?? ''} onChange={(e) => set('passphrase', e.target.value)} />
|
||||
</Field>
|
||||
</>
|
||||
)}
|
||||
|
||||
{c.auth === 'agent' && (
|
||||
<div className="small muted">
|
||||
Uses the agent at <span className="mono">$SSH_AUTH_SOCK</span> of the process running dockmv.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Check
|
||||
checked={c.sudo ?? false}
|
||||
onChange={(v) => set('sudo', v)}
|
||||
label="run docker through sudo -n on the target"
|
||||
title="Needed when the login user is not in the docker group. sudo must not ask for a password."
|
||||
/>
|
||||
|
||||
<Field label="docker command on the target (optional)">
|
||||
<input type="text" placeholder="docker" value={c.dockerCmd ?? ''} onChange={(e) => set('dockerCmd', e.target.value)} />
|
||||
</Field>
|
||||
|
||||
<Check
|
||||
checked={c.saveSecrets ?? false}
|
||||
onChange={(v) => set('saveSecrets', v)}
|
||||
label="remember the password / key on disk"
|
||||
/>
|
||||
{c.saveSecrets ? (
|
||||
<Notice kind="warn">
|
||||
Credentials are stored in plain text in the connections file, readable only by this user. Leave this off to
|
||||
keep them in memory for this session only.
|
||||
</Notice>
|
||||
) : (
|
||||
<div className="small faint">
|
||||
Credentials stay in memory and are lost when dockmv restarts.
|
||||
</div>
|
||||
)}
|
||||
<Field label="label"><input type="text" value={c.name ?? ''} onChange={(e) => set('name', e.target.value)} /></Field>
|
||||
<SshFields value={c} set={set} where="target" />
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user