mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat(multi-server): add actions to the server
This commit is contained in:
@@ -18,6 +18,7 @@ import { useForm } from "react-hook-form";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { validateAndFormatYAML } from "../application/advanced/traefik/update-traefik-config";
|
import { validateAndFormatYAML } from "../application/advanced/traefik/update-traefik-config";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
|
|
||||||
const UpdateServerMiddlewareConfigSchema = z.object({
|
const UpdateServerMiddlewareConfigSchema = z.object({
|
||||||
traefikConfig: z.string(),
|
traefikConfig: z.string(),
|
||||||
@@ -29,12 +30,18 @@ type UpdateServerMiddlewareConfig = z.infer<
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
path: string;
|
path: string;
|
||||||
|
serverId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowTraefikFile = ({ path }: Props) => {
|
export const ShowTraefikFile = ({ path, serverId }: Props) => {
|
||||||
const { data, refetch } = api.settings.readTraefikFile.useQuery(
|
const {
|
||||||
|
data,
|
||||||
|
refetch,
|
||||||
|
isLoading: isLoadingFile,
|
||||||
|
} = api.settings.readTraefikFile.useQuery(
|
||||||
{
|
{
|
||||||
path,
|
path,
|
||||||
|
serverId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !!path,
|
enabled: !!path,
|
||||||
@@ -54,11 +61,9 @@ export const ShowTraefikFile = ({ path }: Props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
form.reset({
|
||||||
form.reset({
|
traefikConfig: data || "",
|
||||||
traefikConfig: data || "",
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [form, form.reset, data]);
|
}, [form, form.reset, data]);
|
||||||
|
|
||||||
const onSubmit = async (data: UpdateServerMiddlewareConfig) => {
|
const onSubmit = async (data: UpdateServerMiddlewareConfig) => {
|
||||||
@@ -74,6 +79,7 @@ export const ShowTraefikFile = ({ path }: Props) => {
|
|||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
traefikConfig: data.traefikConfig,
|
traefikConfig: data.traefikConfig,
|
||||||
path,
|
path,
|
||||||
|
serverId,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Traefik config Updated");
|
toast.success("Traefik config Updated");
|
||||||
@@ -93,20 +99,28 @@ export const ShowTraefikFile = ({ path }: Props) => {
|
|||||||
className="w-full relative z-[5]"
|
className="w-full relative z-[5]"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col overflow-auto">
|
<div className="flex flex-col overflow-auto">
|
||||||
<FormField
|
{isLoadingFile ? (
|
||||||
control={form.control}
|
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
||||||
name="traefikConfig"
|
<span className="text-muted-foreground text-lg font-medium">
|
||||||
render={({ field }) => (
|
Loading...
|
||||||
<FormItem className="relative">
|
</span>
|
||||||
<FormLabel>Traefik config</FormLabel>
|
<Loader2 className="animate-spin size-8 text-muted-foreground" />
|
||||||
<FormDescription className="break-all">
|
</div>
|
||||||
{path}
|
) : (
|
||||||
</FormDescription>
|
<FormField
|
||||||
<FormControl>
|
control={form.control}
|
||||||
<CodeEditor
|
name="traefikConfig"
|
||||||
lineWrapping
|
render={({ field }) => (
|
||||||
wrapperClassName="h-[35rem] font-mono"
|
<FormItem className="relative">
|
||||||
placeholder={`http:
|
<FormLabel>Traefik config</FormLabel>
|
||||||
|
<FormDescription className="break-all">
|
||||||
|
{path}
|
||||||
|
</FormDescription>
|
||||||
|
<FormControl>
|
||||||
|
<CodeEditor
|
||||||
|
lineWrapping
|
||||||
|
wrapperClassName="h-[35rem] font-mono"
|
||||||
|
placeholder={`http:
|
||||||
routers:
|
routers:
|
||||||
router-name:
|
router-name:
|
||||||
rule: Host('domain.com')
|
rule: Host('domain.com')
|
||||||
@@ -116,31 +130,36 @@ routers:
|
|||||||
tls: false
|
tls: false
|
||||||
middlewares: []
|
middlewares: []
|
||||||
`}
|
`}
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</pre>
|
</pre>
|
||||||
<div className="flex justify-end absolute z-50 right-6 top-8">
|
<div className="flex justify-end absolute z-50 right-6 top-8">
|
||||||
<Button
|
<Button
|
||||||
className="shadow-sm"
|
className="shadow-sm"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setCanEdit(!canEdit);
|
setCanEdit(!canEdit);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{canEdit ? "Unlock" : "Lock"}
|
{canEdit ? "Unlock" : "Lock"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button isLoading={isLoading} disabled={canEdit} type="submit">
|
<Button
|
||||||
|
isLoading={isLoading}
|
||||||
|
disabled={canEdit || isLoading}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
Update
|
Update
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,20 +1,38 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { Tree } from "@/components/ui/file-tree";
|
import { Tree } from "@/components/ui/file-tree";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { FileIcon, Folder, Workflow } from "lucide-react";
|
import { FileIcon, Folder, Loader2, Workflow } from "lucide-react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { ShowTraefikFile } from "./show-traefik-file";
|
import { ShowTraefikFile } from "./show-traefik-file";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
|
||||||
export const ShowTraefikSystem = () => {
|
interface Props {
|
||||||
|
serverId?: string;
|
||||||
|
}
|
||||||
|
export const ShowTraefikSystem = ({ serverId }: Props) => {
|
||||||
const [file, setFile] = React.useState<null | string>(null);
|
const [file, setFile] = React.useState<null | string>(null);
|
||||||
|
|
||||||
const { data: directories } = api.settings.readDirectories.useQuery();
|
const {
|
||||||
|
data: directories,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
isError,
|
||||||
|
} = api.settings.readDirectories.useQuery({
|
||||||
|
serverId,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("mt-6 md:grid gap-4")}>
|
<div className={cn("mt-6 md:grid gap-4")}>
|
||||||
<div className="flex flex-col lg:flex-row gap-4 md:gap-10 w-full">
|
<div className="flex flex-col lg:flex-row gap-4 md:gap-10 w-full">
|
||||||
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
{isLoading && (
|
||||||
|
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
||||||
|
<span className="text-muted-foreground text-lg font-medium">
|
||||||
|
Loading...
|
||||||
|
</span>
|
||||||
|
<Loader2 className="animate-spin size-8 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{directories?.length === 0 && (
|
{directories?.length === 0 && (
|
||||||
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
||||||
<span className="text-muted-foreground text-lg font-medium">
|
<span className="text-muted-foreground text-lg font-medium">
|
||||||
@@ -34,7 +52,7 @@ export const ShowTraefikSystem = () => {
|
|||||||
/>
|
/>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
{file ? (
|
{file ? (
|
||||||
<ShowTraefikFile path={file} />
|
<ShowTraefikFile path={file} serverId={serverId} />
|
||||||
) : (
|
) : (
|
||||||
<div className="h-full w-full flex-col gap-2 flex items-center justify-center">
|
<div className="h-full w-full flex-col gap-2 flex items-center justify-center">
|
||||||
<span className="text-muted-foreground text-lg font-medium">
|
<span className="text-muted-foreground text-lg font-medium">
|
||||||
|
|||||||
@@ -49,13 +49,7 @@ export const SetupServer = ({ serverId }: Props) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const { mutateAsync, isLoading } = api.server.setup.useMutation({
|
const { mutateAsync, isLoading } = api.server.setup.useMutation();
|
||||||
onMutate: async (variables) => {
|
|
||||||
console.log("Running....");
|
|
||||||
refetch();
|
|
||||||
// refetch();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
@@ -99,7 +93,7 @@ export const SetupServer = ({ serverId }: Props) => {
|
|||||||
serverId: server?.serverId || "",
|
serverId: server?.serverId || "",
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
// refetch();
|
refetch();
|
||||||
toast.success("Server setup successfully");
|
toast.success("Server setup successfully");
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import {
|
import {
|
||||||
@@ -26,11 +27,45 @@ import { TerminalModal } from "../web-server/terminal-modal";
|
|||||||
import { AddServer } from "./add-server";
|
import { AddServer } from "./add-server";
|
||||||
import { SetupServer } from "./setup-server";
|
import { SetupServer } from "./setup-server";
|
||||||
import { UpdateServer } from "./update-server";
|
import { UpdateServer } from "./update-server";
|
||||||
|
import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal";
|
||||||
|
import { ShowModalLogs } from "../web-server/show-modal-logs";
|
||||||
export const ShowServers = () => {
|
export const ShowServers = () => {
|
||||||
const { data, refetch } = api.server.all.useQuery();
|
const { data, refetch } = api.server.all.useQuery();
|
||||||
const { mutateAsync } = api.server.remove.useMutation();
|
const { mutateAsync } = api.server.remove.useMutation();
|
||||||
const { data: sshKeys } = api.sshKey.all.useQuery();
|
const { data: sshKeys } = api.sshKey.all.useQuery();
|
||||||
|
|
||||||
|
const { mutateAsync: toggleDashboard, isLoading: toggleDashboardIsLoading } =
|
||||||
|
api.settings.toggleDashboard.useMutation();
|
||||||
|
|
||||||
|
const {
|
||||||
|
mutateAsync: cleanDockerBuilder,
|
||||||
|
isLoading: cleanDockerBuilderIsLoading,
|
||||||
|
} = api.settings.cleanDockerBuilder.useMutation();
|
||||||
|
|
||||||
|
const {
|
||||||
|
mutateAsync: cleanUnusedImages,
|
||||||
|
isLoading: cleanUnusedImagesIsLoading,
|
||||||
|
} = api.settings.cleanUnusedImages.useMutation();
|
||||||
|
|
||||||
|
const { mutateAsync: reloadTraefik, isLoading: reloadTraefikIsLoading } =
|
||||||
|
api.settings.reloadTraefik.useMutation();
|
||||||
|
|
||||||
|
const {
|
||||||
|
mutateAsync: cleanUnusedVolumes,
|
||||||
|
isLoading: cleanUnusedVolumesIsLoading,
|
||||||
|
} = api.settings.cleanUnusedVolumes.useMutation();
|
||||||
|
|
||||||
|
const {
|
||||||
|
mutateAsync: cleanStoppedContainers,
|
||||||
|
isLoading: cleanStoppedContainersIsLoading,
|
||||||
|
} = api.settings.cleanStoppedContainers.useMutation();
|
||||||
|
|
||||||
|
const { mutateAsync: cleanAll, isLoading: cleanAllIsLoading } =
|
||||||
|
api.settings.cleanAll.useMutation();
|
||||||
|
|
||||||
|
const { data: haveTraefikDashboardPortEnabled, refetch: refetchDashboard } =
|
||||||
|
api.settings.haveTraefikDashboardPortEnabled.useQuery();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 space-y-6">
|
<div className="p-6 space-y-6">
|
||||||
<div className="space-y-2 flex flex-row justify-between items-end">
|
<div className="space-y-2 flex flex-row justify-between items-end">
|
||||||
@@ -116,7 +151,17 @@ export const ShowServers = () => {
|
|||||||
|
|
||||||
<TableCell className="text-right flex justify-end">
|
<TableCell className="text-right flex justify-end">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger
|
||||||
|
asChild
|
||||||
|
disabled={
|
||||||
|
cleanAllIsLoading ||
|
||||||
|
cleanDockerBuilderIsLoading ||
|
||||||
|
cleanUnusedImagesIsLoading ||
|
||||||
|
cleanUnusedVolumesIsLoading ||
|
||||||
|
cleanStoppedContainersIsLoading ||
|
||||||
|
reloadTraefikIsLoading
|
||||||
|
}
|
||||||
|
>
|
||||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||||
<span className="sr-only">Open menu</span>
|
<span className="sr-only">Open menu</span>
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
@@ -128,6 +173,7 @@ export const ShowServers = () => {
|
|||||||
<span>Enter the terminal</span>
|
<span>Enter the terminal</span>
|
||||||
</TerminalModal>
|
</TerminalModal>
|
||||||
<SetupServer serverId={server.serverId} />
|
<SetupServer serverId={server.serverId} />
|
||||||
|
|
||||||
<UpdateServer serverId={server.serverId} />
|
<UpdateServer serverId={server.serverId} />
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title={"Delete Server"}
|
title={"Delete Server"}
|
||||||
@@ -154,6 +200,150 @@ export const ShowServers = () => {
|
|||||||
Delete Server
|
Delete Server
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuLabel>Traefik</DropdownMenuLabel>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={async () => {
|
||||||
|
await reloadTraefik({
|
||||||
|
serverId: server.serverId,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Traefik Reloaded");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to reload the traefik");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>Reload</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<ShowTraefikFileSystemModal
|
||||||
|
serverId={server.serverId}
|
||||||
|
/>
|
||||||
|
<ShowModalLogs
|
||||||
|
appName="dokploy-traefik"
|
||||||
|
serverId={server.serverId}
|
||||||
|
>
|
||||||
|
<span>Watch logs</span>
|
||||||
|
</ShowModalLogs>
|
||||||
|
|
||||||
|
{/* <DropdownMenuItem
|
||||||
|
onClick={async () => {
|
||||||
|
await toggleDashboard({
|
||||||
|
enableDashboard:
|
||||||
|
!haveTraefikDashboardPortEnabled,
|
||||||
|
serverId: server.serverId,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success(
|
||||||
|
`${haveTraefikDashboardPortEnabled ? "Disabled" : "Enabled"} Dashboard`,
|
||||||
|
);
|
||||||
|
refetchDashboard();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error(
|
||||||
|
`${haveTraefikDashboardPortEnabled ? "Disabled" : "Enabled"} Dashboard`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="w-full cursor-pointer space-x-3"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{haveTraefikDashboardPortEnabled
|
||||||
|
? "Disable"
|
||||||
|
: "Enable"}{" "}
|
||||||
|
Dashboard
|
||||||
|
</span>
|
||||||
|
</DropdownMenuItem> */}
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
|
<DropdownMenuLabel>Storage</DropdownMenuLabel>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer"
|
||||||
|
onClick={async () => {
|
||||||
|
await cleanUnusedImages({
|
||||||
|
serverId: server?.serverId,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Cleaned images");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to clean images");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>Clean unused images</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer"
|
||||||
|
onClick={async () => {
|
||||||
|
await cleanUnusedVolumes({
|
||||||
|
serverId: server?.serverId,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Cleaned volumes");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to clean volumes");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>Clean unused volumes</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer"
|
||||||
|
onClick={async () => {
|
||||||
|
await cleanStoppedContainers({
|
||||||
|
serverId: server?.serverId,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Stopped containers cleaned");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error(
|
||||||
|
"Error to clean stopped containers",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>Clean stopped containers</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer"
|
||||||
|
onClick={async () => {
|
||||||
|
await cleanDockerBuilder({
|
||||||
|
serverId: server?.serverId,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Cleaned Docker Builder");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error(
|
||||||
|
"Error to clean Docker Builder",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>Clean Docker Builder & System</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer"
|
||||||
|
onClick={async () => {
|
||||||
|
await cleanAll({
|
||||||
|
serverId: server?.serverId,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Cleaned all");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to clean all");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>Clean all</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||||
|
import { FileTextIcon } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { ShowTraefikSystem } from "../../file-system/show-traefik-system";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
serverId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShowTraefikFileSystemModal = ({ serverId }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer "
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
Show Traefik File System
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-7xl overflow-y-auto max-h-screen ">
|
||||||
|
<DialogHeader>
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<FileTextIcon className="size-5" /> Traefik File System
|
||||||
|
</DialogTitle>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
See all the files and directories of your traefik configuration
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div id="hook-form-add-gitlab" className="grid w-full gap-1">
|
||||||
|
<ShowTraefikSystem serverId={serverId} />
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -33,12 +33,15 @@ type Schema = z.infer<typeof schema>;
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
serverId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EditTraefikEnv = ({ children }: Props) => {
|
export const EditTraefikEnv = ({ children, serverId }: Props) => {
|
||||||
const [canEdit, setCanEdit] = useState(true);
|
const [canEdit, setCanEdit] = useState(true);
|
||||||
|
|
||||||
const { data } = api.settings.readTraefikEnv.useQuery();
|
const { data } = api.settings.readTraefikEnv.useQuery({
|
||||||
|
serverId,
|
||||||
|
});
|
||||||
|
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
api.settings.writeTraefikEnv.useMutation();
|
api.settings.writeTraefikEnv.useMutation();
|
||||||
|
|||||||
@@ -36,12 +36,14 @@ export const DockerLogsId = dynamic(
|
|||||||
interface Props {
|
interface Props {
|
||||||
appName: string;
|
appName: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
serverId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowModalLogs = ({ appName, children }: Props) => {
|
export const ShowModalLogs = ({ appName, children, serverId }: Props) => {
|
||||||
const { data, isLoading } = api.docker.getContainersByAppLabel.useQuery(
|
const { data, isLoading } = api.docker.getContainersByAppLabel.useQuery(
|
||||||
{
|
{
|
||||||
appName,
|
appName,
|
||||||
|
serverId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !!appName,
|
enabled: !!appName,
|
||||||
@@ -96,7 +98,11 @@ export const ShowModalLogs = ({ appName, children }: Props) => {
|
|||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<DockerLogsId id="terminal" containerId={containerId || ""} />
|
<DockerLogsId
|
||||||
|
id="terminal"
|
||||||
|
containerId={containerId || ""}
|
||||||
|
serverId={serverId}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -55,9 +55,10 @@ export const dockerRouter = createTRPCRouter({
|
|||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
appName: z.string().min(1),
|
appName: z.string().min(1),
|
||||||
|
serverId: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
return await getContainersByAppLabel(input.appName);
|
return await getContainersByAppLabel(input.appName, input.serverId);
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
apiReadStatsLogs,
|
apiReadStatsLogs,
|
||||||
apiReadTraefikConfig,
|
apiReadTraefikConfig,
|
||||||
apiSaveSSHKey,
|
apiSaveSSHKey,
|
||||||
|
apiStorage,
|
||||||
apiTraefikConfig,
|
apiTraefikConfig,
|
||||||
apiUpdateDockerCleanup,
|
apiUpdateDockerCleanup,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
@@ -20,11 +21,13 @@ import {
|
|||||||
cleanUpUnusedVolumes,
|
cleanUpUnusedVolumes,
|
||||||
prepareEnvironmentVariables,
|
prepareEnvironmentVariables,
|
||||||
startService,
|
startService,
|
||||||
|
startServiceRemote,
|
||||||
stopService,
|
stopService,
|
||||||
|
stopServiceRemote,
|
||||||
} from "@/server/utils/docker/utils";
|
} from "@/server/utils/docker/utils";
|
||||||
import { recreateDirectory } from "@/server/utils/filesystem/directory";
|
import { recreateDirectory } from "@/server/utils/filesystem/directory";
|
||||||
import { sendDockerCleanupNotifications } from "@/server/utils/notifications/docker-cleanup";
|
import { sendDockerCleanupNotifications } from "@/server/utils/notifications/docker-cleanup";
|
||||||
import { execAsync } from "@/server/utils/process/execAsync";
|
import { execAsync, execAsyncRemote } from "@/server/utils/process/execAsync";
|
||||||
import { spawnAsync } from "@/server/utils/process/spawnAsync";
|
import { spawnAsync } from "@/server/utils/process/spawnAsync";
|
||||||
import {
|
import {
|
||||||
readConfig,
|
readConfig,
|
||||||
@@ -63,16 +66,23 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
await execAsync(`docker service update --force ${stdout.trim()}`);
|
await execAsync(`docker service update --force ${stdout.trim()}`);
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
reloadTraefik: adminProcedure.mutation(async () => {
|
reloadTraefik: adminProcedure
|
||||||
try {
|
.input(apiStorage)
|
||||||
await stopService("dokploy-traefik");
|
.mutation(async ({ input }) => {
|
||||||
await startService("dokploy-traefik");
|
try {
|
||||||
} catch (err) {
|
if (input?.serverId) {
|
||||||
console.error(err);
|
await stopServiceRemote(input.serverId, "dokploy-traefik");
|
||||||
}
|
await startServiceRemote(input.serverId, "dokploy-traefik");
|
||||||
|
} else {
|
||||||
|
await stopService("dokploy-traefik");
|
||||||
|
await startService("dokploy-traefik");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
toggleDashboard: adminProcedure
|
toggleDashboard: adminProcedure
|
||||||
.input(apiEnableDashboard)
|
.input(apiEnableDashboard)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
@@ -82,31 +92,42 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
cleanUnusedImages: adminProcedure.mutation(async () => {
|
cleanUnusedImages: adminProcedure
|
||||||
await cleanUpUnusedImages();
|
.input(apiStorage)
|
||||||
return true;
|
.mutation(async ({ input }) => {
|
||||||
}),
|
await cleanUpUnusedImages(input?.serverId);
|
||||||
cleanUnusedVolumes: adminProcedure.mutation(async () => {
|
return true;
|
||||||
await cleanUpUnusedVolumes();
|
}),
|
||||||
return true;
|
cleanUnusedVolumes: adminProcedure
|
||||||
}),
|
.input(apiStorage)
|
||||||
cleanStoppedContainers: adminProcedure.mutation(async () => {
|
.mutation(async ({ input }) => {
|
||||||
await cleanStoppedContainers();
|
await cleanUpUnusedVolumes(input?.serverId);
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
cleanDockerBuilder: adminProcedure.mutation(async () => {
|
cleanStoppedContainers: adminProcedure
|
||||||
await cleanUpDockerBuilder();
|
.input(apiStorage)
|
||||||
}),
|
.mutation(async ({ input }) => {
|
||||||
cleanDockerPrune: adminProcedure.mutation(async () => {
|
await cleanStoppedContainers(input?.serverId);
|
||||||
await cleanUpSystemPrune();
|
return true;
|
||||||
await cleanUpDockerBuilder();
|
}),
|
||||||
|
cleanDockerBuilder: adminProcedure
|
||||||
|
.input(apiStorage)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
await cleanUpDockerBuilder(input?.serverId);
|
||||||
|
}),
|
||||||
|
cleanDockerPrune: adminProcedure
|
||||||
|
.input(apiStorage)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
await cleanUpSystemPrune(input?.serverId);
|
||||||
|
await cleanUpDockerBuilder(input?.serverId);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
cleanAll: adminProcedure.mutation(async () => {
|
cleanAll: adminProcedure.input(apiStorage).mutation(async ({ input }) => {
|
||||||
await cleanUpUnusedImages();
|
await cleanUpUnusedImages(input?.serverId);
|
||||||
await cleanUpDockerBuilder();
|
await cleanStoppedContainers(input?.serverId);
|
||||||
await cleanUpSystemPrune();
|
await cleanUpDockerBuilder(input?.serverId);
|
||||||
|
await cleanUpSystemPrune(input?.serverId);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
@@ -230,18 +251,20 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
getDokployVersion: adminProcedure.query(() => {
|
getDokployVersion: adminProcedure.query(() => {
|
||||||
return getDokployVersion();
|
return getDokployVersion();
|
||||||
}),
|
}),
|
||||||
readDirectories: protectedProcedure.query(async ({ ctx }) => {
|
readDirectories: protectedProcedure
|
||||||
if (ctx.user.rol === "user") {
|
.input(apiStorage)
|
||||||
const canAccess = await canAccessToTraefikFiles(ctx.user.authId);
|
.query(async ({ ctx, input }) => {
|
||||||
|
if (ctx.user.rol === "user") {
|
||||||
|
const canAccess = await canAccessToTraefikFiles(ctx.user.authId);
|
||||||
|
|
||||||
if (!canAccess) {
|
if (!canAccess) {
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
const { MAIN_TRAEFIK_PATH } = paths(!!input?.serverId);
|
||||||
const { MAIN_TRAEFIK_PATH } = paths();
|
const result = await readDirectory(MAIN_TRAEFIK_PATH, input?.serverId);
|
||||||
const result = readDirectory(MAIN_TRAEFIK_PATH);
|
return result || [];
|
||||||
return result || [];
|
}),
|
||||||
}),
|
|
||||||
|
|
||||||
updateTraefikFile: protectedProcedure
|
updateTraefikFile: protectedProcedure
|
||||||
.input(apiModifyTraefikConfig)
|
.input(apiModifyTraefikConfig)
|
||||||
@@ -253,7 +276,11 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
writeTraefikConfigInPath(input.path, input.traefikConfig);
|
await writeTraefikConfigInPath(
|
||||||
|
input.path,
|
||||||
|
input.traefikConfig,
|
||||||
|
input?.serverId,
|
||||||
|
);
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -267,7 +294,7 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return readConfigInPath(input.path);
|
return readConfigInPath(input.path, input.serverId);
|
||||||
}),
|
}),
|
||||||
getIp: protectedProcedure.query(async () => {
|
getIp: protectedProcedure.query(async () => {
|
||||||
const admin = await findAdmin();
|
const admin = await findAdmin();
|
||||||
@@ -324,16 +351,20 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
return openApiDocument;
|
return openApiDocument;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
readTraefikEnv: adminProcedure.query(async () => {
|
readTraefikEnv: adminProcedure.input(apiStorage).query(async ({ input }) => {
|
||||||
const { stdout } = await execAsync(
|
const command =
|
||||||
"docker service inspect --format='{{range .Spec.TaskTemplate.ContainerSpec.Env}}{{println .}}{{end}}' dokploy-traefik",
|
"docker service inspect --format='{{range .Spec.TaskTemplate.ContainerSpec.Env}}{{println .}}{{end}}' dokploy-traefik";
|
||||||
);
|
|
||||||
|
|
||||||
return stdout.trim();
|
if (input?.serverId) {
|
||||||
|
const result = await execAsyncRemote(input.serverId, command);
|
||||||
|
return result.stdout.trim();
|
||||||
|
}
|
||||||
|
const result = await execAsync(command);
|
||||||
|
return result.stdout.trim();
|
||||||
}),
|
}),
|
||||||
|
|
||||||
writeTraefikEnv: adminProcedure
|
writeTraefikEnv: adminProcedure
|
||||||
.input(z.object({ env: z.string() }))
|
.input(z.object({ env: z.string(), serverId: z.string().optional() }))
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const envs = prepareEnvironmentVariables(input.env);
|
const envs = prepareEnvironmentVariables(input.env);
|
||||||
await initializeTraefik({
|
await initializeTraefik({
|
||||||
|
|||||||
@@ -126,12 +126,25 @@ export const getContainersByAppNameMatch = async (
|
|||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getContainersByAppLabel = async (appName: string) => {
|
export const getContainersByAppLabel = async (
|
||||||
|
appName: string,
|
||||||
|
serverId?: string,
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
const { stdout, stderr } = await execAsync(
|
let stdout = "";
|
||||||
`docker ps --filter "label=com.docker.swarm.service.name=${appName}" --format 'CONTAINER ID : {{.ID}} | Name: {{.Names}} | State: {{.State}}'`,
|
let stderr = "";
|
||||||
);
|
|
||||||
|
|
||||||
|
const command = `docker ps --filter "label=com.docker.swarm.service.name=${appName}" --format 'CONTAINER ID : {{.ID}} | Name: {{.Names}} | State: {{.State}}'`;
|
||||||
|
console.log(command);
|
||||||
|
if (serverId) {
|
||||||
|
const result = await execAsyncRemote(serverId, command);
|
||||||
|
stdout = result.stdout;
|
||||||
|
stderr = result.stderr;
|
||||||
|
} else {
|
||||||
|
const result = await execAsync(command);
|
||||||
|
stdout = result.stdout;
|
||||||
|
stderr = result.stderr;
|
||||||
|
}
|
||||||
if (stderr) {
|
if (stderr) {
|
||||||
console.error(`Error: ${stderr}`);
|
console.error(`Error: ${stderr}`);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { join } from "node:path";
|
|||||||
import { docker } from "@/server/constants";
|
import { docker } from "@/server/constants";
|
||||||
import { getServiceContainer } from "@/server/utils/docker/utils";
|
import { getServiceContainer } from "@/server/utils/docker/utils";
|
||||||
import packageInfo from "../../../package.json";
|
import packageInfo from "../../../package.json";
|
||||||
|
import { execAsyncRemote } from "@/server/utils/process/execAsync";
|
||||||
|
|
||||||
const updateIsAvailable = async () => {
|
const updateIsAvailable = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -44,8 +45,66 @@ interface TreeDataItem {
|
|||||||
children?: TreeDataItem[];
|
children?: TreeDataItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const readDirectory = (dirPath: string): TreeDataItem[] => {
|
export const readDirectory = async (
|
||||||
|
dirPath: string,
|
||||||
|
serverId?: string,
|
||||||
|
): Promise<TreeDataItem[]> => {
|
||||||
|
if (serverId) {
|
||||||
|
const { stdout } = await execAsyncRemote(
|
||||||
|
serverId,
|
||||||
|
`
|
||||||
|
process_items() {
|
||||||
|
local parent_dir="$1"
|
||||||
|
local __resultvar=$2
|
||||||
|
|
||||||
|
local items_json=""
|
||||||
|
local first=true
|
||||||
|
for item in "$parent_dir"/*; do
|
||||||
|
[ -e "$item" ] || continue
|
||||||
|
process_item "$item" item_json
|
||||||
|
if [ "$first" = true ]; then
|
||||||
|
first=false
|
||||||
|
items_json="$item_json"
|
||||||
|
else
|
||||||
|
items_json="$items_json,$item_json"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
eval $__resultvar="'[$items_json]'"
|
||||||
|
}
|
||||||
|
|
||||||
|
process_item() {
|
||||||
|
local item_path="$1"
|
||||||
|
local __resultvar=$2
|
||||||
|
|
||||||
|
local item_name=$(basename "$item_path")
|
||||||
|
local escaped_name=$(echo "$item_name" | sed 's/"/\\"/g')
|
||||||
|
local escaped_path=$(echo "$item_path" | sed 's/"/\\"/g')
|
||||||
|
|
||||||
|
if [ -d "$item_path" ]; then
|
||||||
|
# Is directory
|
||||||
|
process_items "$item_path" children_json
|
||||||
|
local json='{"id":"'"$escaped_path"'","name":"'"$escaped_name"'","type":"directory","children":'"$children_json"'}'
|
||||||
|
else
|
||||||
|
# Is file
|
||||||
|
local json='{"id":"'"$escaped_path"'","name":"'"$escaped_name"'","type":"file"}'
|
||||||
|
fi
|
||||||
|
|
||||||
|
eval $__resultvar="'$json'"
|
||||||
|
}
|
||||||
|
|
||||||
|
root_dir=${dirPath}
|
||||||
|
|
||||||
|
process_items "$root_dir" json_output
|
||||||
|
|
||||||
|
echo "$json_output"
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
const result = JSON.parse(stdout);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
const items = readdirSync(dirPath, { withFileTypes: true });
|
const items = readdirSync(dirPath, { withFileTypes: true });
|
||||||
|
|
||||||
return items.map((item) => {
|
return items.map((item) => {
|
||||||
const fullPath = join(dirPath, item.name);
|
const fullPath = join(dirPath, item.name);
|
||||||
if (item.isDirectory()) {
|
if (item.isDirectory()) {
|
||||||
@@ -61,5 +120,5 @@ export const readDirectory = (dirPath: string): TreeDataItem[] => {
|
|||||||
name: item.name,
|
name: item.name,
|
||||||
type: "file",
|
type: "file",
|
||||||
};
|
};
|
||||||
});
|
}) as unknown as Promise<TreeDataItem[]>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -72,15 +72,24 @@ export const apiTraefikConfig = z.object({
|
|||||||
export const apiModifyTraefikConfig = z.object({
|
export const apiModifyTraefikConfig = z.object({
|
||||||
path: z.string().min(1),
|
path: z.string().min(1),
|
||||||
traefikConfig: z.string().min(1),
|
traefikConfig: z.string().min(1),
|
||||||
|
serverId: z.string().optional(),
|
||||||
});
|
});
|
||||||
export const apiReadTraefikConfig = z.object({
|
export const apiReadTraefikConfig = z.object({
|
||||||
path: z.string().min(1),
|
path: z.string().min(1),
|
||||||
|
serverId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apiEnableDashboard = z.object({
|
export const apiEnableDashboard = z.object({
|
||||||
enableDashboard: z.boolean().optional(),
|
enableDashboard: z.boolean().optional(),
|
||||||
|
serverId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const apiStorage = z
|
||||||
|
.object({
|
||||||
|
serverId: z.string().optional(),
|
||||||
|
})
|
||||||
|
.optional();
|
||||||
|
|
||||||
export const apiReadStatsLogs = z.object({
|
export const apiReadStatsLogs = z.object({
|
||||||
page: z
|
page: z
|
||||||
.object({
|
.object({
|
||||||
|
|||||||
@@ -149,27 +149,39 @@ export const getContainerByName = (name: string): Promise<ContainerInfo> => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
export const cleanUpUnusedImages = async () => {
|
export const cleanUpUnusedImages = async (serverId?: string) => {
|
||||||
try {
|
try {
|
||||||
await execAsync("docker image prune --all --force");
|
if (serverId) {
|
||||||
|
await execAsyncRemote(serverId, "docker image prune --all --force");
|
||||||
|
} else {
|
||||||
|
await execAsync("docker image prune --all --force");
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cleanStoppedContainers = async () => {
|
export const cleanStoppedContainers = async (serverId?: string) => {
|
||||||
try {
|
try {
|
||||||
await execAsync("docker container prune --force");
|
if (serverId) {
|
||||||
|
await execAsyncRemote(serverId, "docker container prune --force");
|
||||||
|
} else {
|
||||||
|
await execAsync("docker container prune --force");
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cleanUpUnusedVolumes = async () => {
|
export const cleanUpUnusedVolumes = async (serverId?: string) => {
|
||||||
try {
|
try {
|
||||||
await execAsync("docker volume prune --all --force");
|
if (serverId) {
|
||||||
|
await execAsyncRemote(serverId, "docker volume prune --all --force");
|
||||||
|
} else {
|
||||||
|
await execAsync("docker volume prune --all --force");
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -193,12 +205,23 @@ export const cleanUpInactiveContainers = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cleanUpDockerBuilder = async () => {
|
export const cleanUpDockerBuilder = async (serverId?: string) => {
|
||||||
await execAsync("docker builder prune --all --force");
|
if (serverId) {
|
||||||
|
await execAsyncRemote(serverId, "docker builder prune --all --force");
|
||||||
|
} else {
|
||||||
|
await execAsync("docker builder prune --all --force");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cleanUpSystemPrune = async () => {
|
export const cleanUpSystemPrune = async (serverId?: string) => {
|
||||||
await execAsync("docker system prune --all --force --volumes");
|
if (serverId) {
|
||||||
|
await execAsyncRemote(
|
||||||
|
serverId,
|
||||||
|
"docker system prune --all --force --volumes",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await execAsync("docker system prune --all --force --volumes");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const startService = async (appName: string) => {
|
export const startService = async (appName: string) => {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { paths } from "@/server/constants";
|
|||||||
import { dump, load } from "js-yaml";
|
import { dump, load } from "js-yaml";
|
||||||
import { execAsyncRemote } from "../process/execAsync";
|
import { execAsyncRemote } from "../process/execAsync";
|
||||||
import type { FileConfig, HttpLoadBalancerService } from "./file-types";
|
import type { FileConfig, HttpLoadBalancerService } from "./file-types";
|
||||||
|
import { encodeBase64 } from "../docker/utils";
|
||||||
|
|
||||||
export const createTraefikConfig = (appName: string) => {
|
export const createTraefikConfig = (appName: string) => {
|
||||||
const defaultPort = 3000;
|
const defaultPort = 3000;
|
||||||
@@ -146,8 +147,14 @@ export const readMonitoringConfig = () => {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const readConfigInPath = (pathFile: string) => {
|
export const readConfigInPath = async (pathFile: string, serverId?: string) => {
|
||||||
const configPath = path.join(pathFile);
|
const configPath = path.join(pathFile);
|
||||||
|
|
||||||
|
if (serverId) {
|
||||||
|
const { stdout } = await execAsyncRemote(serverId, `cat ${configPath}`);
|
||||||
|
if (!stdout) return null;
|
||||||
|
return stdout;
|
||||||
|
}
|
||||||
if (fs.existsSync(configPath)) {
|
if (fs.existsSync(configPath)) {
|
||||||
const yamlStr = fs.readFileSync(configPath, "utf8");
|
const yamlStr = fs.readFileSync(configPath, "utf8");
|
||||||
return yamlStr;
|
return yamlStr;
|
||||||
@@ -179,12 +186,22 @@ export const writeConfigRemote = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const writeTraefikConfigInPath = (
|
export const writeTraefikConfigInPath = async (
|
||||||
pathFile: string,
|
pathFile: string,
|
||||||
traefikConfig: string,
|
traefikConfig: string,
|
||||||
|
serverId?: string,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const configPath = path.join(pathFile);
|
const configPath = path.join(pathFile);
|
||||||
|
if (serverId) {
|
||||||
|
const encoded = encodeBase64(traefikConfig);
|
||||||
|
await execAsyncRemote(
|
||||||
|
serverId,
|
||||||
|
`echo "${encoded}" | base64 -d > "${configPath}"`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
fs.writeFileSync(configPath, traefikConfig, "utf8");
|
||||||
|
}
|
||||||
fs.writeFileSync(configPath, traefikConfig, "utf8");
|
fs.writeFileSync(configPath, traefikConfig, "utf8");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error saving the YAML config file:", e);
|
console.error("Error saving the YAML config file:", e);
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import { spawn } from "node-pty";
|
|||||||
import { WebSocketServer } from "ws";
|
import { WebSocketServer } from "ws";
|
||||||
import { validateWebSocketRequest } from "../auth/auth";
|
import { validateWebSocketRequest } from "../auth/auth";
|
||||||
import { getShell } from "./utils";
|
import { getShell } from "./utils";
|
||||||
|
import { Client } from "ssh2";
|
||||||
|
import { readSSHKey } from "../utils/filesystem/ssh";
|
||||||
|
import { findServerById } from "../api/services/server";
|
||||||
|
|
||||||
export const setupDockerContainerTerminalWebSocketServer = (
|
export const setupDockerContainerTerminalWebSocketServer = (
|
||||||
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
|
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
|
||||||
@@ -44,7 +47,123 @@ export const setupDockerContainerTerminalWebSocketServer = (
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (serverId) {
|
if (serverId) {
|
||||||
// const server = await findServerById(serverId);
|
const server = await findServerById(serverId);
|
||||||
|
if (!server.sshKeyId)
|
||||||
|
throw new Error("No SSH key available for this server");
|
||||||
|
|
||||||
|
const keys = await readSSHKey(server.sshKeyId);
|
||||||
|
const conn = new Client();
|
||||||
|
let stdout = "";
|
||||||
|
let stderr = "";
|
||||||
|
// conn
|
||||||
|
// .once("ready", () => {
|
||||||
|
// console.log("Client :: ready");
|
||||||
|
// conn.exec(
|
||||||
|
// `docker run -i ${containerId} ${activeWay}`,
|
||||||
|
// (err, stream) => {
|
||||||
|
// if (err) throw err;
|
||||||
|
|
||||||
|
// stream
|
||||||
|
// .on("close", (code: number, signal: string) => {
|
||||||
|
// console.log(
|
||||||
|
// `Stream :: close :: code: ${code}, signal: ${signal}`,
|
||||||
|
// );
|
||||||
|
// conn.end();
|
||||||
|
// })
|
||||||
|
// .on("data", (data: string) => {
|
||||||
|
// stdout += data.toString();
|
||||||
|
// ws.send(data.toString());
|
||||||
|
// })
|
||||||
|
// .stderr.on("data", (data) => {
|
||||||
|
// stderr += data.toString();
|
||||||
|
// ws.send(data.toString());
|
||||||
|
// console.error("Error: ", data.toString());
|
||||||
|
// });
|
||||||
|
|
||||||
|
// // Maneja la entrada de comandos desde WebSocket
|
||||||
|
// ws.on("message", (message) => {
|
||||||
|
// try {
|
||||||
|
// let command: string | Buffer[] | Buffer | ArrayBuffer;
|
||||||
|
// if (Buffer.isBuffer(message)) {
|
||||||
|
// command = message.toString("utf8");
|
||||||
|
// } else {
|
||||||
|
// command = message;
|
||||||
|
// }
|
||||||
|
// stream.write(command.toString());
|
||||||
|
// } catch (error) {
|
||||||
|
// // @ts-ignore
|
||||||
|
// const errorMessage = error?.message as unknown as string;
|
||||||
|
// ws.send(errorMessage);
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// // Cuando se cierra la conexión WebSocket
|
||||||
|
// ws.on("close", () => {
|
||||||
|
// stream.end();
|
||||||
|
// });
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
// })p
|
||||||
|
// .connect({
|
||||||
|
// host: server.ipAddress,
|
||||||
|
// port: server.port,
|
||||||
|
// username: server.username,
|
||||||
|
// privateKey: keys.privateKey,
|
||||||
|
// timeout: 99999,
|
||||||
|
// });
|
||||||
|
// conn
|
||||||
|
// .once("ready", () => {
|
||||||
|
// console.log("Client :: ready");
|
||||||
|
// conn.shell((err, stream) => {
|
||||||
|
// if (err) throw err;
|
||||||
|
|
||||||
|
// stream
|
||||||
|
// .on("close", (code: number, signal: string) => {
|
||||||
|
// console.log(
|
||||||
|
// `Stream :: close :: code: ${code}, signal: ${signal}`,
|
||||||
|
// );
|
||||||
|
// conn.end();
|
||||||
|
// })
|
||||||
|
// .on("data", (data: string) => {
|
||||||
|
// stdout += data.toString();
|
||||||
|
// ws.send(data.toString());
|
||||||
|
// })
|
||||||
|
// .stderr.on("data", (data) => {
|
||||||
|
// stderr += data.toString();
|
||||||
|
// ws.send(data.toString());
|
||||||
|
// console.error("Error: ", data.toString());
|
||||||
|
// });
|
||||||
|
// stream.write(`docker exec -it ${containerId} ${activeWay}\n`);
|
||||||
|
// // Maneja la entrada de comandos desde WebSocket
|
||||||
|
// ws.on("message", (message) => {
|
||||||
|
// try {
|
||||||
|
// let command: string | Buffer[] | Buffer | ArrayBuffer;
|
||||||
|
// if (Buffer.isBuffer(message)) {
|
||||||
|
// command = message.toString("utf8");
|
||||||
|
// } else {
|
||||||
|
// command = message;
|
||||||
|
// }
|
||||||
|
// stream.write(command.toString());
|
||||||
|
// } catch (error) {
|
||||||
|
// // @ts-ignore
|
||||||
|
// const errorMessage = error?.message as unknown as string;
|
||||||
|
// ws.send(errorMessage);
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// // Cuando se cierra la conexión WebSocket
|
||||||
|
// ws.on("close", () => {
|
||||||
|
// stream.end();
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// })
|
||||||
|
// .connect({
|
||||||
|
// host: server.ipAddress,
|
||||||
|
// port: server.port,
|
||||||
|
// username: server.username,
|
||||||
|
// privateKey: keys.privateKey,
|
||||||
|
// timeout: 99999,
|
||||||
|
// });
|
||||||
} else {
|
} else {
|
||||||
const shell = getShell();
|
const shell = getShell();
|
||||||
const ptyProcess = spawn(
|
const ptyProcess = spawn(
|
||||||
|
|||||||
Reference in New Issue
Block a user