mirror of
https://github.com/andwati/dokploy.git
synced 2026-10-18 03:02:56 +00:00
Merge pull request #5024 from Dokploy/fix/stack-containers-worker-nodes-4520
fix: show stack containers running on worker nodes
This commit is contained in:
@@ -114,6 +114,7 @@ export const ShowComposeContainers = ({
|
|||||||
<TableHead>Name</TableHead>
|
<TableHead>Name</TableHead>
|
||||||
<TableHead>State</TableHead>
|
<TableHead>State</TableHead>
|
||||||
<TableHead>Status</TableHead>
|
<TableHead>Status</TableHead>
|
||||||
|
{appType === "stack" && <TableHead>Node</TableHead>}
|
||||||
<TableHead>Container ID</TableHead>
|
<TableHead>Container ID</TableHead>
|
||||||
<TableHead className="text-right" />
|
<TableHead className="text-right" />
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -121,8 +122,9 @@ export const ShowComposeContainers = ({
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{data.map((container) => (
|
{data.map((container) => (
|
||||||
<ContainerRow
|
<ContainerRow
|
||||||
key={container.containerId}
|
key={container.name}
|
||||||
container={container}
|
container={container}
|
||||||
|
appType={appType}
|
||||||
serverId={serverId}
|
serverId={serverId}
|
||||||
serviceId={serviceId}
|
serviceId={serviceId}
|
||||||
onActionComplete={() => refetch()}
|
onActionComplete={() => refetch()}
|
||||||
@@ -143,7 +145,9 @@ interface ContainerRowProps {
|
|||||||
name: string;
|
name: string;
|
||||||
state: string;
|
state: string;
|
||||||
status: string;
|
status: string;
|
||||||
|
node?: string;
|
||||||
};
|
};
|
||||||
|
appType: "stack" | "docker-compose";
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
serviceId?: string;
|
serviceId?: string;
|
||||||
onActionComplete: () => void;
|
onActionComplete: () => void;
|
||||||
@@ -151,6 +155,7 @@ interface ContainerRowProps {
|
|||||||
|
|
||||||
const ContainerRow = ({
|
const ContainerRow = ({
|
||||||
container,
|
container,
|
||||||
|
appType,
|
||||||
serverId,
|
serverId,
|
||||||
serviceId,
|
serviceId,
|
||||||
onActionComplete,
|
onActionComplete,
|
||||||
@@ -192,7 +197,13 @@ const ContainerRow = ({
|
|||||||
variant={
|
variant={
|
||||||
container.state === "running"
|
container.state === "running"
|
||||||
? "default"
|
? "default"
|
||||||
: container.state === "exited"
|
: [
|
||||||
|
"exited",
|
||||||
|
"pending",
|
||||||
|
"preparing",
|
||||||
|
"starting",
|
||||||
|
"ready",
|
||||||
|
].includes(container.state)
|
||||||
? "secondary"
|
? "secondary"
|
||||||
: "destructive"
|
: "destructive"
|
||||||
}
|
}
|
||||||
@@ -201,96 +212,99 @@ const ContainerRow = ({
|
|||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{container.status}</TableCell>
|
<TableCell>{container.status}</TableCell>
|
||||||
|
{appType === "stack" && <TableCell>{container.node || "-"}</TableCell>}
|
||||||
<TableCell className="font-mono text-sm text-muted-foreground">
|
<TableCell className="font-mono text-sm text-muted-foreground">
|
||||||
{container.containerId}
|
{container.containerId || "-"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
<Dialog open={logsOpen} onOpenChange={setLogsOpen}>
|
{!container.containerId ? null : (
|
||||||
<DropdownMenu>
|
<Dialog open={logsOpen} onOpenChange={setLogsOpen}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenu>
|
||||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
<DropdownMenuTrigger asChild>
|
||||||
{actionLoading ? (
|
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
{actionLoading ? (
|
||||||
) : (
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
) : (
|
||||||
)}
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
</Button>
|
)}
|
||||||
</DropdownMenuTrigger>
|
</Button>
|
||||||
<DropdownMenuContent align="end">
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
<DropdownMenuContent align="end">
|
||||||
<DialogTrigger asChild>
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="cursor-pointer"
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
View Logs
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DialogTrigger>
|
||||||
|
<ShowContainerConfig
|
||||||
|
containerId={container.containerId}
|
||||||
|
serverId={serverId || ""}
|
||||||
|
/>
|
||||||
|
<ShowContainerMounts
|
||||||
|
containerId={container.containerId}
|
||||||
|
serverId={serverId || ""}
|
||||||
|
/>
|
||||||
|
<ShowContainerNetworks
|
||||||
|
containerId={container.containerId}
|
||||||
|
serverId={serverId || ""}
|
||||||
|
/>
|
||||||
|
<DockerTerminalModal
|
||||||
|
containerId={container.containerId}
|
||||||
|
serverId={serverId || ""}
|
||||||
|
serviceId={serviceId}
|
||||||
|
>
|
||||||
|
Terminal
|
||||||
|
</DockerTerminalModal>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onSelect={(e) => e.preventDefault()}
|
disabled={actionLoading !== null}
|
||||||
|
onClick={() => handleAction("restart", restartMutation)}
|
||||||
>
|
>
|
||||||
View Logs
|
Restart
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogTrigger>
|
<DropdownMenuItem
|
||||||
<ShowContainerConfig
|
className="cursor-pointer"
|
||||||
containerId={container.containerId}
|
disabled={actionLoading !== null}
|
||||||
serverId={serverId || ""}
|
onClick={() => handleAction("start", startMutation)}
|
||||||
/>
|
>
|
||||||
<ShowContainerMounts
|
Start
|
||||||
containerId={container.containerId}
|
</DropdownMenuItem>
|
||||||
serverId={serverId || ""}
|
<DropdownMenuItem
|
||||||
/>
|
className="cursor-pointer"
|
||||||
<ShowContainerNetworks
|
disabled={actionLoading !== null}
|
||||||
containerId={container.containerId}
|
onClick={() => handleAction("stop", stopMutation)}
|
||||||
serverId={serverId || ""}
|
>
|
||||||
/>
|
Stop
|
||||||
<DockerTerminalModal
|
</DropdownMenuItem>
|
||||||
containerId={container.containerId}
|
<DropdownMenuItem
|
||||||
serverId={serverId || ""}
|
className="cursor-pointer text-red-500 focus:text-red-600"
|
||||||
serviceId={serviceId}
|
disabled={actionLoading !== null}
|
||||||
>
|
onClick={() => handleAction("kill", killMutation)}
|
||||||
Terminal
|
>
|
||||||
</DockerTerminalModal>
|
Kill
|
||||||
<DropdownMenuSeparator />
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
</DropdownMenuContent>
|
||||||
className="cursor-pointer"
|
</DropdownMenu>
|
||||||
disabled={actionLoading !== null}
|
<DialogContent className="sm:max-w-7xl">
|
||||||
onClick={() => handleAction("restart", restartMutation)}
|
<DialogHeader>
|
||||||
>
|
<DialogTitle>View Logs</DialogTitle>
|
||||||
Restart
|
<DialogDescription>Logs for {container.name}</DialogDescription>
|
||||||
</DropdownMenuItem>
|
</DialogHeader>
|
||||||
<DropdownMenuItem
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
className="cursor-pointer"
|
<DockerLogsId
|
||||||
disabled={actionLoading !== null}
|
containerId={container.containerId}
|
||||||
onClick={() => handleAction("start", startMutation)}
|
serverId={serverId}
|
||||||
>
|
runType="native"
|
||||||
Start
|
serviceId={serviceId}
|
||||||
</DropdownMenuItem>
|
/>
|
||||||
<DropdownMenuItem
|
</div>
|
||||||
className="cursor-pointer"
|
</DialogContent>
|
||||||
disabled={actionLoading !== null}
|
</Dialog>
|
||||||
onClick={() => handleAction("stop", stopMutation)}
|
)}
|
||||||
>
|
|
||||||
Stop
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="cursor-pointer text-red-500 focus:text-red-600"
|
|
||||||
disabled={actionLoading !== null}
|
|
||||||
onClick={() => handleAction("kill", killMutation)}
|
|
||||||
>
|
|
||||||
Kill
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
<DialogContent className="sm:max-w-7xl">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>View Logs</DialogTitle>
|
|
||||||
<DialogDescription>Logs for {container.name}</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
|
||||||
<DockerLogsId
|
|
||||||
containerId={container.containerId}
|
|
||||||
serverId={serverId}
|
|
||||||
runType="native"
|
|
||||||
serviceId={serviceId}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export const ShowDockerLogsStack = ({
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: containers, isPending: containersLoading } =
|
const { data, isPending: containersLoading } =
|
||||||
api.docker.getContainersByAppNameMatch.useQuery(
|
api.docker.getContainersByAppNameMatch.useQuery(
|
||||||
{
|
{
|
||||||
appName,
|
appName,
|
||||||
@@ -69,6 +69,8 @@ export const ShowDockerLogsStack = ({
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const containers = data?.filter((container) => container.containerId);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (option === "native") {
|
if (option === "native") {
|
||||||
if (containers && containers?.length > 0) {
|
if (containers && containers?.length > 0) {
|
||||||
|
|||||||
@@ -108,6 +108,9 @@ export const getContainersByAppNameMatch = async (
|
|||||||
serverId?: string,
|
serverId?: string,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
|
if (appType === "stack") {
|
||||||
|
return await getStackTaskContainers(appName, serverId);
|
||||||
|
}
|
||||||
let result: string[] = [];
|
let result: string[] = [];
|
||||||
const cmd =
|
const cmd =
|
||||||
"docker ps -a --format 'CONTAINER ID : {{.ID}} | Name: {{.Names}} | State: {{.State}} | Status: {{.Status}}'";
|
"docker ps -a --format 'CONTAINER ID : {{.ID}} | Name: {{.Names}} | State: {{.State}} | Status: {{.Status}}'";
|
||||||
@@ -166,6 +169,66 @@ export const getContainersByAppNameMatch = async (
|
|||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getStackTaskContainers = async (appName: string, serverId?: string) => {
|
||||||
|
try {
|
||||||
|
const divider = "__DOKPLOY_DIVIDER__";
|
||||||
|
const tasksCommand = `docker stack ps ${appName} --no-trunc --filter "desired-state=running" --format 'TASK : {{.ID}} | Name: {{.Name}} | Node: {{.Node}} | CurrentState: {{.CurrentState}} | Error: {{.Error}}'`;
|
||||||
|
const inspectCommand = `docker stack ps ${appName} -q --no-trunc --filter "desired-state=running" | xargs -r docker inspect --format '{{if .Status.ContainerStatus}}TASK : {{.ID}} | ContainerId: {{.Status.ContainerStatus.ContainerID}}{{end}}' 2>/dev/null`;
|
||||||
|
const command = `${tasksCommand} && echo "${divider}" && (${inspectCommand} || true)`;
|
||||||
|
|
||||||
|
let stdout = "";
|
||||||
|
|
||||||
|
if (serverId) {
|
||||||
|
const result = await execAsyncRemote(serverId, command);
|
||||||
|
stdout = result.stdout;
|
||||||
|
} else {
|
||||||
|
const result = await execAsync(command);
|
||||||
|
stdout = result.stdout;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stdout) return [];
|
||||||
|
|
||||||
|
const [tasksSection = "", inspectSection = ""] = stdout.split(divider);
|
||||||
|
|
||||||
|
const containerIdByTask = new Map<string, string>();
|
||||||
|
for (const line of inspectSection.trim().split("\n")) {
|
||||||
|
const parts = line.split(" | ");
|
||||||
|
const taskId = parts[0]?.replace("TASK : ", "").trim();
|
||||||
|
const containerId = parts[1]?.replace("ContainerId: ", "").trim();
|
||||||
|
if (taskId && containerId) {
|
||||||
|
containerIdByTask.set(taskId, containerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const containers = [];
|
||||||
|
|
||||||
|
for (const line of tasksSection.trim().split("\n")) {
|
||||||
|
if (!line) continue;
|
||||||
|
const parts = line.split(" | ");
|
||||||
|
const taskId = parts[0]?.replace("TASK : ", "").trim() ?? "";
|
||||||
|
const name = parts[1]?.replace("Name: ", "").trim() ?? "";
|
||||||
|
const node = parts[2]?.replace("Node: ", "").trim() ?? "";
|
||||||
|
const currentState = parts[3]
|
||||||
|
? parts[3].replace("CurrentState: ", "").trim()
|
||||||
|
: "";
|
||||||
|
const error = parts[4] ? parts[4].replace("Error: ", "").trim() : "";
|
||||||
|
const containerId = containerIdByTask.get(taskId) ?? "";
|
||||||
|
|
||||||
|
containers.push({
|
||||||
|
containerId: containerId.slice(0, 12),
|
||||||
|
name: `${name}.${taskId}`,
|
||||||
|
node,
|
||||||
|
state: currentState.split(" ")[0]?.toLowerCase() ?? "No state",
|
||||||
|
status: error ? `${currentState} (${error})` : currentState,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return containers;
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
export const getStackContainersByAppName = async (
|
export const getStackContainersByAppName = async (
|
||||||
appName: string,
|
appName: string,
|
||||||
serverId?: string,
|
serverId?: string,
|
||||||
@@ -175,7 +238,6 @@ export const getStackContainersByAppName = async (
|
|||||||
|
|
||||||
const command = `docker stack ps ${appName} --no-trunc --format 'CONTAINER ID : {{.ID}} | Name: {{.Name}} | State: {{.DesiredState}} | Node: {{.Node}} | CurrentState: {{.CurrentState}} | Error: {{.Error}}'`;
|
const command = `docker stack ps ${appName} --no-trunc --format 'CONTAINER ID : {{.ID}} | Name: {{.Name}} | State: {{.DesiredState}} | Node: {{.Node}} | CurrentState: {{.CurrentState}} | Error: {{.Error}}'`;
|
||||||
|
|
||||||
console.log("command ", command);
|
|
||||||
if (serverId) {
|
if (serverId) {
|
||||||
const { stdout, stderr } = await execAsyncRemote(serverId, command);
|
const { stdout, stderr } = await execAsyncRemote(serverId, command);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user