fix: use host bind mount for /data/repos so tool containers can access mounted repos
The API container used a named Docker volume (repo_data:/data/repos) for storing repositories. When creating tool instances with direct mount mode, the API told Docker to bind-mount /data/repos/<repo>:/workspace into the tool container. But the Docker daemon resolves bind-mount paths on the HOST filesystem, not inside the API container. Since the host had no /data/repos (the repos only existed inside the named volume), tool containers mounted empty directories. Changed both compose files to use a host bind mount (/data/repos:/data/repos) instead of a named volume. This ensures: - The API container and tool containers both see the same /data/repos path - Bind mounts from /data/repos into tool containers work correctly For existing installations: repos previously stored in the repo_data named volume should be copied to /data/repos on the host before restarting the stack. Quality gates: compose file syntax valid
This commit is contained in:
@@ -1,165 +1,187 @@
|
|||||||
import { useState, useCallback } from "react";
|
import { useState, useCallback } from "react";
|
||||||
import {
|
import {
|
||||||
stopInstance,
|
stopInstance,
|
||||||
deleteInstance,
|
deleteInstance,
|
||||||
startInstance,
|
startInstance,
|
||||||
recreateInstanceTunnel,
|
recreateInstanceTunnel,
|
||||||
} from "../api/sessions";
|
} from "../api/sessions";
|
||||||
import type { Session } from "../api/sessions";
|
import type { Session } from "../api/sessions";
|
||||||
|
|
||||||
interface UseInstanceActionsOptions {
|
interface UseInstanceActionsOptions {
|
||||||
onRefresh: () => Promise<void>;
|
onRefresh: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UseInstanceActionsReturn {
|
interface UseInstanceActionsReturn {
|
||||||
loadingSessionId: string | null;
|
loadingSessionId: string | null;
|
||||||
dirtyDeleteSession: Session | null;
|
dirtyDeleteSession: Session | null;
|
||||||
dirtyDeleteFiles: string[];
|
dirtyDeleteFiles: string[];
|
||||||
handleOpen: (session: Session) => void;
|
handleOpen: (session: Session) => void;
|
||||||
handleStart: (session: Session) => Promise<void>;
|
handleStart: (session: Session) => Promise<void>;
|
||||||
handleStop: (session: Session) => Promise<void>;
|
handleStop: (session: Session) => Promise<void>;
|
||||||
handleDelete: (session: Session) => Promise<void>;
|
handleDelete: (session: Session) => Promise<void>;
|
||||||
handleForceDelete: (session: Session) => Promise<void>;
|
handleForceDelete: (session: Session) => Promise<void>;
|
||||||
handleRecreateTunnel: (session: Session) => Promise<void>;
|
handleRecreateTunnel: (session: Session) => Promise<void>;
|
||||||
clearDirtyDelete: () => void;
|
clearDirtyDelete: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useInstanceActions(
|
export function useInstanceActions(
|
||||||
options: UseInstanceActionsOptions
|
options: UseInstanceActionsOptions,
|
||||||
): UseInstanceActionsReturn {
|
): UseInstanceActionsReturn {
|
||||||
const { onRefresh } = options;
|
const { onRefresh } = options;
|
||||||
const [loadingSessionId, setLoadingSessionId] = useState<string | null>(null);
|
const [loadingSessionId, setLoadingSessionId] = useState<string | null>(null);
|
||||||
const [dirtyDeleteSession, setDirtyDeleteSession] = useState<Session | null>(null);
|
const [dirtyDeleteSession, setDirtyDeleteSession] = useState<Session | null>(
|
||||||
const [dirtyDeleteFiles, setDirtyDeleteFiles] = useState<string[]>([]);
|
null,
|
||||||
|
);
|
||||||
|
const [dirtyDeleteFiles, setDirtyDeleteFiles] = useState<string[]>([]);
|
||||||
|
|
||||||
const handleOpen = useCallback((session: Session) => {
|
const handleOpen = useCallback((session: Session) => {
|
||||||
if (session.url) {
|
if (session.url) {
|
||||||
window.open(session.url, "_blank", "noopener,noreferrer");
|
window.open(session.url, "_blank", "noopener,noreferrer");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (session.tool_type_interfaces?.includes("terminal")) {
|
if (session.tool_type_interfaces?.includes("terminal")) {
|
||||||
window.location.href = `/instances/${session.id}/terminal`;
|
window.location.href = `/instances/${session.id}/terminal`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
window.location.href = `/projects/${session.project_id}`;
|
window.location.href = `/projects/${session.project_id}`;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleStart = useCallback(
|
const handleStart = useCallback(
|
||||||
async (session: Session) => {
|
async (session: Session) => {
|
||||||
if (loadingSessionId === session.id) return;
|
if (loadingSessionId === session.id) return;
|
||||||
setLoadingSessionId(session.id);
|
setLoadingSessionId(session.id);
|
||||||
try {
|
try {
|
||||||
await startInstance(session.project_id, session.repository_id, session.id);
|
await startInstance(
|
||||||
await onRefresh();
|
session.project_id,
|
||||||
} catch {
|
session.repository_id,
|
||||||
// ignore
|
session.id,
|
||||||
} finally {
|
);
|
||||||
setLoadingSessionId(null);
|
await onRefresh();
|
||||||
}
|
} catch {
|
||||||
},
|
// ignore
|
||||||
[loadingSessionId, onRefresh]
|
} finally {
|
||||||
);
|
setLoadingSessionId(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[loadingSessionId, onRefresh],
|
||||||
|
);
|
||||||
|
|
||||||
const handleStop = useCallback(
|
const handleStop = useCallback(
|
||||||
async (session: Session) => {
|
async (session: Session) => {
|
||||||
if (loadingSessionId === session.id) return;
|
if (loadingSessionId === session.id) return;
|
||||||
setLoadingSessionId(session.id);
|
setLoadingSessionId(session.id);
|
||||||
try {
|
try {
|
||||||
await stopInstance(session.project_id, session.repository_id, session.id);
|
await stopInstance(
|
||||||
await onRefresh();
|
session.project_id,
|
||||||
} catch {
|
session.repository_id,
|
||||||
// ignore
|
session.id,
|
||||||
} finally {
|
);
|
||||||
setLoadingSessionId(null);
|
await onRefresh();
|
||||||
}
|
} catch {
|
||||||
},
|
// ignore
|
||||||
[loadingSessionId, onRefresh]
|
} finally {
|
||||||
);
|
setLoadingSessionId(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[loadingSessionId, onRefresh],
|
||||||
|
);
|
||||||
|
|
||||||
const handleDelete = useCallback(
|
const handleDelete = useCallback(
|
||||||
async (session: Session) => {
|
async (session: Session) => {
|
||||||
if (loadingSessionId === session.id) return;
|
if (loadingSessionId === session.id) return;
|
||||||
setLoadingSessionId(session.id);
|
setLoadingSessionId(session.id);
|
||||||
try {
|
try {
|
||||||
await deleteInstance(session.project_id, session.repository_id, session.id);
|
await deleteInstance(
|
||||||
setDirtyDeleteSession(null);
|
session.project_id,
|
||||||
setDirtyDeleteFiles([]);
|
session.repository_id,
|
||||||
await onRefresh();
|
session.id,
|
||||||
} catch (error) {
|
);
|
||||||
const axiosError = error as {
|
setDirtyDeleteSession(null);
|
||||||
response?: { status?: number; data?: { detail?: { changed_files?: string[] } } };
|
setDirtyDeleteFiles([]);
|
||||||
};
|
await onRefresh();
|
||||||
if (axiosError.response?.status === 409) {
|
} catch (error) {
|
||||||
const detail = axiosError.response.data?.detail;
|
const axiosError = error as {
|
||||||
if (detail?.changed_files) {
|
response?: {
|
||||||
setDirtyDeleteSession(session);
|
status?: number;
|
||||||
setDirtyDeleteFiles(detail.changed_files);
|
data?: { detail?: { changed_files?: string[] } };
|
||||||
return;
|
};
|
||||||
}
|
};
|
||||||
}
|
if (axiosError.response?.status === 409) {
|
||||||
} finally {
|
const detail = axiosError.response.data?.detail;
|
||||||
setLoadingSessionId(null);
|
if (detail?.changed_files) {
|
||||||
}
|
setDirtyDeleteSession(session);
|
||||||
},
|
setDirtyDeleteFiles(detail.changed_files);
|
||||||
[loadingSessionId, onRefresh]
|
return;
|
||||||
);
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoadingSessionId(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[loadingSessionId, onRefresh],
|
||||||
|
);
|
||||||
|
|
||||||
const handleForceDelete = useCallback(
|
const handleForceDelete = useCallback(
|
||||||
async (session: Session) => {
|
async (session: Session) => {
|
||||||
if (loadingSessionId === session.id) return;
|
if (loadingSessionId === session.id) return;
|
||||||
setLoadingSessionId(session.id);
|
setLoadingSessionId(session.id);
|
||||||
try {
|
try {
|
||||||
await deleteInstance(session.project_id, session.repository_id, session.id, true);
|
await deleteInstance(
|
||||||
setDirtyDeleteSession(null);
|
session.project_id,
|
||||||
setDirtyDeleteFiles([]);
|
session.repository_id,
|
||||||
await onRefresh();
|
session.id,
|
||||||
} catch {
|
true,
|
||||||
// ignore
|
);
|
||||||
} finally {
|
setDirtyDeleteSession(null);
|
||||||
setLoadingSessionId(null);
|
setDirtyDeleteFiles([]);
|
||||||
}
|
await onRefresh();
|
||||||
},
|
} catch {
|
||||||
[loadingSessionId, onRefresh]
|
// ignore
|
||||||
);
|
} finally {
|
||||||
|
setLoadingSessionId(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[loadingSessionId, onRefresh],
|
||||||
|
);
|
||||||
|
|
||||||
const handleRecreateTunnel = useCallback(
|
const handleRecreateTunnel = useCallback(
|
||||||
async (session: Session) => {
|
async (session: Session) => {
|
||||||
if (loadingSessionId === session.id) return;
|
if (loadingSessionId === session.id) return;
|
||||||
setLoadingSessionId(session.id);
|
setLoadingSessionId(session.id);
|
||||||
try {
|
try {
|
||||||
await recreateInstanceTunnel(
|
await recreateInstanceTunnel(
|
||||||
session.project_id,
|
session.project_id,
|
||||||
session.repository_id,
|
session.repository_id,
|
||||||
session.id
|
session.id,
|
||||||
);
|
);
|
||||||
await onRefresh();
|
await onRefresh();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message =
|
const message =
|
||||||
(err as { response?: { data?: { detail?: string } } })?.response?.data
|
(err as { response?: { data?: { detail?: string } } })?.response?.data
|
||||||
?.detail || "Failed to recreate tunnel";
|
?.detail || "Failed to recreate tunnel";
|
||||||
alert(message);
|
alert(message);
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingSessionId(null);
|
setLoadingSessionId(null);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[loadingSessionId, onRefresh]
|
[loadingSessionId, onRefresh],
|
||||||
);
|
);
|
||||||
|
|
||||||
const clearDirtyDelete = useCallback(() => {
|
const clearDirtyDelete = useCallback(() => {
|
||||||
setDirtyDeleteSession(null);
|
setDirtyDeleteSession(null);
|
||||||
setDirtyDeleteFiles([]);
|
setDirtyDeleteFiles([]);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
loadingSessionId,
|
loadingSessionId,
|
||||||
dirtyDeleteSession,
|
dirtyDeleteSession,
|
||||||
dirtyDeleteFiles,
|
dirtyDeleteFiles,
|
||||||
handleOpen,
|
handleOpen,
|
||||||
handleStart,
|
handleStart,
|
||||||
handleStop,
|
handleStop,
|
||||||
handleDelete,
|
handleDelete,
|
||||||
handleForceDelete,
|
handleForceDelete,
|
||||||
handleRecreateTunnel,
|
handleRecreateTunnel,
|
||||||
clearDirtyDelete,
|
clearDirtyDelete,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ services:
|
|||||||
AUTHENTIK_AUTHORIZE_URL: ${AUTHENTIK_AUTHORIZE_URL:-}
|
AUTHENTIK_AUTHORIZE_URL: ${AUTHENTIK_AUTHORIZE_URL:-}
|
||||||
AUTHENTIK_TOKEN_URL: ${AUTHENTIK_TOKEN_URL:-}
|
AUTHENTIK_TOKEN_URL: ${AUTHENTIK_TOKEN_URL:-}
|
||||||
volumes:
|
volumes:
|
||||||
- repo_data:/data/repos
|
- /data/repos:/data/repos
|
||||||
- /data/instances:/data/instances
|
- /data/instances:/data/instances
|
||||||
- avatar_uploads:/app/uploads
|
- avatar_uploads:/app/uploads
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
@@ -116,7 +116,6 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
redis_data:
|
redis_data:
|
||||||
repo_data:
|
|
||||||
avatar_uploads:
|
avatar_uploads:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
+1
-2
@@ -57,7 +57,7 @@ services:
|
|||||||
REPO_BASE_PATH: /data/repos
|
REPO_BASE_PATH: /data/repos
|
||||||
INSTANCE_BASE_PATH: /data/instances
|
INSTANCE_BASE_PATH: /data/instances
|
||||||
volumes:
|
volumes:
|
||||||
- repo_data:/data/repos
|
- /data/repos:/data/repos
|
||||||
- /data/instances:/data/instances
|
- /data/instances:/data/instances
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
@@ -91,7 +91,6 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
redis_data:
|
redis_data:
|
||||||
repo_data:
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
backend:
|
backend:
|
||||||
|
|||||||
Reference in New Issue
Block a user