feat: add ssh_key_id to config profiles for container key mounting

- Add ssh_key_id column to ConfigProfile model and migration
- Update config profile API to accept/return ssh_key_id
- Include ssh_key_id in ResolvedProfile and resolver logic
- Mount selected SSH key into container home dir at start_instance
- Frontend config profile form with SSH key selector dropdown
- Git mount URL validation defaults to profile's SSH key

Quality gates: pytest (231 passed, 6 pre-existing), tsc --noEmit clean
This commit is contained in:
Alex Blank
2026-05-29 12:53:51 +02:00
parent d413fb84a5
commit 57ff236f2d
11 changed files with 232 additions and 26 deletions
+39 -18
View File
@@ -6,6 +6,7 @@ import type { GitMount, GitMountMapping } from "../api/config_profiles";
interface GitMountEditorProps {
mounts: GitMount[];
onChange: (mounts: GitMount[]) => void;
defaultSshKeyId?: string;
}
function normalizeMount(mount: GitMount): GitMount {
@@ -33,7 +34,7 @@ function normalizeMounts(mounts: GitMount[]): GitMount[] {
return mounts.map(normalizeMount);
}
export const GitMountEditor = ({ mounts, onChange }: GitMountEditorProps) => {
export const GitMountEditor = ({ mounts, onChange, defaultSshKeyId }: GitMountEditorProps) => {
const [normalizedMounts, setNormalizedMounts] = useState<GitMount[]>(() =>
normalizeMounts(mounts),
);
@@ -92,6 +93,7 @@ export const GitMountEditor = ({ mounts, onChange }: GitMountEditorProps) => {
mount={mount}
onSave={(updated) => handleUpdate(index, updated)}
onCancel={() => setEditingIndex(null)}
defaultSshKeyId={defaultSshKeyId}
/>
) : (
<div>
@@ -183,6 +185,7 @@ export const GitMountEditor = ({ mounts, onChange }: GitMountEditorProps) => {
}}
onSave={handleAdd}
onCancel={() => setIsAdding(false)}
defaultSshKeyId={defaultSshKeyId}
/>
</div>
) : (
@@ -203,6 +206,7 @@ interface GitMountFormProps {
mount: GitMount;
onSave: (mount: GitMount) => void;
onCancel: () => void;
defaultSshKeyId?: string;
}
type ValidationState =
@@ -212,7 +216,7 @@ type ValidationState =
| { status: "suggestion"; suggestedUrl: string; message: string }
| { status: "invalid"; message: string };
const GitMountForm = ({ mount, onSave, onCancel }: GitMountFormProps) => {
const GitMountForm = ({ mount, onSave, onCancel, defaultSshKeyId }: GitMountFormProps) => {
const [remoteUrl, setRemoteUrl] = useState(mount.remote_url);
const [branch, setBranch] = useState(mount.branch || "");
const [mappings, setMappings] = useState<GitMountMapping[]>(
@@ -221,7 +225,9 @@ const GitMountForm = ({ mount, onSave, onCancel }: GitMountFormProps) => {
: [{ source_path: ".", target_path: "" }],
);
const [errors, setErrors] = useState<Record<string, string>>({});
const [validation, setValidation] = useState<ValidationState>({ status: "idle" });
const [validation, setValidation] = useState<ValidationState>({
status: "idle",
});
const isUrlValidated =
validation.status === "valid" ||
@@ -239,7 +245,10 @@ const GitMountForm = ({ mount, onSave, onCancel }: GitMountFormProps) => {
return next;
});
try {
const result = await validateGitUrl(remoteUrl.trim());
const result = await validateGitUrl(
remoteUrl.trim(),
defaultSshKeyId,
);
if (result.valid && result.branches) {
setValidation({
status: "valid",
@@ -351,7 +360,10 @@ const GitMountForm = ({ mount, onSave, onCancel }: GitMountFormProps) => {
return (
<div style={{ display: "flex", flexDirection: "column", gap: "0.75rem" }}>
<div className="form-row" style={{ gap: "0.5rem", alignItems: "flex-start" }}>
<div
className="form-row"
style={{ gap: "0.5rem", alignItems: "flex-start" }}
>
<div style={{ flex: 2 }}>
<label style={{ fontSize: "0.875rem", fontWeight: 500 }}>
Repository URL
@@ -393,16 +405,19 @@ const GitMountForm = ({ mount, onSave, onCancel }: GitMountFormProps) => {
)}
{validation.status === "valid" && (
<span className="validation-status valid">
Repository is accessible ({(validation as Extract<ValidationState, { status: "valid" }>).branches.length} branches)
Repository is accessible (
{
(validation as Extract<ValidationState, { status: "valid" }>)
.branches.length
}{" "}
branches)
</span>
)}
{validation.status === "suggestion" && (
<div className="url-suggestion">
<span>{validation.message}</span>
<div className="suggestion-actions">
<code className="suggested-url">
{validation.suggestedUrl}
</code>
<code className="suggested-url">{validation.suggestedUrl}</code>
<button
type="button"
className="secondary-button small"
@@ -429,13 +444,13 @@ const GitMountForm = ({ mount, onSave, onCancel }: GitMountFormProps) => {
onChange={(e) => setBranch(e.target.value)}
className="form-input"
>
{(validation as Extract<ValidationState, { status: "valid" }>).branches.map(
(b) => (
<option key={b} value={b}>
{b}
</option>
),
)}
{(
validation as Extract<ValidationState, { status: "valid" }>
).branches.map((b) => (
<option key={b} value={b}>
{b}
</option>
))}
</select>
) : (
<input
@@ -450,7 +465,12 @@ const GitMountForm = ({ mount, onSave, onCancel }: GitMountFormProps) => {
</div>
</div>
<div style={{ opacity: isUrlValidated ? 1 : 0.5, pointerEvents: isUrlValidated ? "auto" : "none" }}>
<div
style={{
opacity: isUrlValidated ? 1 : 0.5,
pointerEvents: isUrlValidated ? "auto" : "none",
}}
>
<label style={{ fontSize: "0.875rem", fontWeight: 500 }}>
Mappings
</label>
@@ -461,7 +481,8 @@ const GitMountForm = ({ mount, onSave, onCancel }: GitMountFormProps) => {
Source paths within the repo and where to mount them in the container.
{!isUrlValidated && (
<span style={{ color: "var(--warning)" }}>
{" "}Validate the URL first.
{" "}
Validate the URL first.
</span>
)}
</p>