mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-13 01:45:23 +02:00
Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f61fb3aba0 | ||
|
|
d3b7e68da9 | ||
|
|
061ca6c95c | ||
|
|
e576c1a63f | ||
|
|
5d53cf4090 | ||
|
|
ff27f0828b | ||
|
|
33d4f57611 | ||
|
|
bacadccaa9 | ||
|
|
55748749fd | ||
|
|
45b75fdfde | ||
|
|
ff822481c5 | ||
|
|
783324628f | ||
|
|
e70c476c9f | ||
|
|
891260fe41 | ||
|
|
062037a9e6 | ||
|
|
7da1be877b | ||
|
|
60e6285e8e | ||
|
|
cd8c67bb9b | ||
|
|
4fb3ad3032 | ||
|
|
736a7320d4 | ||
|
|
23b235303c | ||
|
|
eb8c6e4367 | ||
|
|
965f05c7c8 | ||
|
|
e316beaddb | ||
|
|
8aff1e7614 | ||
|
|
dbe1733dcb | ||
|
|
73d87c06e1 | ||
|
|
e136934cbc | ||
|
|
4840abe3a4 | ||
|
|
f046ba427a | ||
|
|
b12e84c645 | ||
|
|
d18fe8390b | ||
|
|
e88a9ce96f | ||
|
|
a5abd46386 | ||
|
|
ad0e044740 | ||
|
|
7a0ff72f51 | ||
|
|
2e702dc41f | ||
|
|
766f9244da | ||
|
|
6413fa54e6 | ||
|
|
1c9dcc0c9e | ||
|
|
fee802a57b | ||
|
|
af2b053caa | ||
|
|
42a4cc7fff | ||
|
|
2a7807c2b3 | ||
|
|
153390ff26 | ||
|
|
425b8ec3c2 | ||
|
|
e86caccfd5 | ||
|
|
8a93116ce0 | ||
|
|
daff2adb02 | ||
|
|
052fc5ffe1 | ||
|
|
d08fdeb939 | ||
|
|
8ca8839d7e | ||
|
|
b2264a9148 | ||
|
|
f7ddc715c7 | ||
|
|
63568a4887 | ||
|
|
4b44bc86b4 |
@@ -46,7 +46,7 @@ COPY --from=build /prod/dokploy/node_modules ./node_modules
|
|||||||
|
|
||||||
|
|
||||||
# Install docker
|
# Install docker
|
||||||
RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm get-docker.sh && curl https://rclone.org/install.sh | bash
|
RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh --version 28.5.2 && rm get-docker.sh && curl https://rclone.org/install.sh | bash
|
||||||
|
|
||||||
# Install Nixpacks and tsx
|
# Install Nixpacks and tsx
|
||||||
# | VERBOSE=1 VERSION=1.21.0 bash
|
# | VERBOSE=1 VERSION=1.21.0 bash
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
DATABASE_URL="postgres://dokploy:amukds4wi9001583845717ad2@localhost:5432/dokploy"
|
DATABASE_URL="postgres://dokploy:amukds4wi9001583845717ad2@localhost:5432/dokploy"
|
||||||
PORT=3000
|
PORT=3000
|
||||||
NODE_ENV=development
|
NODE_ENV=development
|
||||||
|
|||||||
311
apps/dokploy/__test__/env/environment.test.ts
vendored
311
apps/dokploy/__test__/env/environment.test.ts
vendored
@@ -1,4 +1,7 @@
|
|||||||
import { prepareEnvironmentVariables } from "@dokploy/server/index";
|
import {
|
||||||
|
prepareEnvironmentVariables,
|
||||||
|
prepareEnvironmentVariablesForShell,
|
||||||
|
} from "@dokploy/server/index";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
const projectEnv = `
|
const projectEnv = `
|
||||||
@@ -332,4 +335,310 @@ IS_DEV=\${{environment.DEVELOPMENT}}
|
|||||||
"IS_DEV=0",
|
"IS_DEV=0",
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("handles environment variables with single quotes in values", () => {
|
||||||
|
const envWithSingleQuotes = `
|
||||||
|
ENV_VARIABLE='ENVITONME'NT'
|
||||||
|
ANOTHER_VAR='value with 'quotes' inside'
|
||||||
|
SIMPLE_VAR=no-quotes
|
||||||
|
`;
|
||||||
|
|
||||||
|
const serviceWithSingleQuotes = `
|
||||||
|
TEST_VAR=\${{environment.ENV_VARIABLE}}
|
||||||
|
ANOTHER_TEST=\${{environment.ANOTHER_VAR}}
|
||||||
|
SIMPLE=\${{environment.SIMPLE_VAR}}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const resolved = prepareEnvironmentVariables(
|
||||||
|
serviceWithSingleQuotes,
|
||||||
|
"",
|
||||||
|
envWithSingleQuotes,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
"TEST_VAR=ENVITONME'NT",
|
||||||
|
"ANOTHER_TEST=value with 'quotes' inside",
|
||||||
|
"SIMPLE=no-quotes",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("prepareEnvironmentVariablesForShell (shell escaping)", () => {
|
||||||
|
it("escapes single quotes in environment variable values", () => {
|
||||||
|
const serviceEnv = `
|
||||||
|
ENV_VARIABLE='ENVITONME'NT'
|
||||||
|
ANOTHER_VAR='value with 'quotes' inside'
|
||||||
|
`;
|
||||||
|
|
||||||
|
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
|
||||||
|
|
||||||
|
// shell-quote should wrap these in double quotes
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
`"ENV_VARIABLE=ENVITONME'NT"`,
|
||||||
|
`"ANOTHER_VAR=value with 'quotes' inside"`,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("escapes double quotes in environment variable values", () => {
|
||||||
|
const serviceEnv = `
|
||||||
|
MESSAGE="Hello "World""
|
||||||
|
QUOTED_PATH="/path/to/"file""
|
||||||
|
`;
|
||||||
|
|
||||||
|
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
|
||||||
|
|
||||||
|
// shell-quote wraps in single quotes when there are double quotes inside
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
`'MESSAGE=Hello "World"'`,
|
||||||
|
`'QUOTED_PATH=/path/to/"file"'`,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("escapes dollar signs in environment variable values", () => {
|
||||||
|
const serviceEnv = `
|
||||||
|
PRICE=$100
|
||||||
|
VARIABLE=$HOME/path
|
||||||
|
TEMPLATE=Hello $USER
|
||||||
|
`;
|
||||||
|
|
||||||
|
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
|
||||||
|
|
||||||
|
// Dollar signs should be escaped to prevent variable expansion
|
||||||
|
for (const env of resolved) {
|
||||||
|
expect(env).toContain("$");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("escapes backticks in environment variable values", () => {
|
||||||
|
const serviceEnv = `
|
||||||
|
COMMAND=\`echo "test"\`
|
||||||
|
NESTED=value with \`backticks\` inside
|
||||||
|
`;
|
||||||
|
|
||||||
|
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
|
||||||
|
|
||||||
|
// Backticks are escaped/removed by dotenv parsing, but values should be safely quoted
|
||||||
|
expect(resolved.length).toBe(2);
|
||||||
|
expect(resolved[0]).toContain("COMMAND");
|
||||||
|
expect(resolved[1]).toContain("NESTED");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles environment variables with spaces", () => {
|
||||||
|
const serviceEnv = `
|
||||||
|
FULL_NAME="John Doe"
|
||||||
|
MESSAGE='Hello World'
|
||||||
|
SENTENCE=This is a test
|
||||||
|
`;
|
||||||
|
|
||||||
|
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
|
||||||
|
|
||||||
|
// shell-quote uses single quotes for strings with spaces
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
`'FULL_NAME=John Doe'`,
|
||||||
|
`'MESSAGE=Hello World'`,
|
||||||
|
`'SENTENCE=This is a test'`,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles environment variables with backslashes", () => {
|
||||||
|
const serviceEnv = `
|
||||||
|
WINDOWS_PATH=C:\\Users\\Documents
|
||||||
|
ESCAPED=value\\with\\backslashes
|
||||||
|
`;
|
||||||
|
|
||||||
|
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
|
||||||
|
|
||||||
|
// Backslashes should be properly escaped
|
||||||
|
expect(resolved.length).toBe(2);
|
||||||
|
for (const env of resolved) {
|
||||||
|
expect(env).toContain("\\");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles simple environment variables without special characters", () => {
|
||||||
|
const serviceEnv = `
|
||||||
|
NODE_ENV=production
|
||||||
|
PORT=3000
|
||||||
|
DEBUG=true
|
||||||
|
`;
|
||||||
|
|
||||||
|
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
|
||||||
|
|
||||||
|
// shell-quote escapes the = sign in some cases
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
"NODE_ENV\\=production",
|
||||||
|
"PORT\\=3000",
|
||||||
|
"DEBUG\\=true",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles environment variables with mixed special characters", () => {
|
||||||
|
const serviceEnv = `
|
||||||
|
COMPLEX='value with "double" and 'single' quotes'
|
||||||
|
BASH_COMMAND=echo "$HOME" && echo 'test'
|
||||||
|
WEIRD=\`echo "$VAR"\` with 'quotes' and "more"
|
||||||
|
`;
|
||||||
|
|
||||||
|
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
|
||||||
|
|
||||||
|
// All should be escaped, none should throw errors
|
||||||
|
expect(resolved.length).toBe(3);
|
||||||
|
// Verify each can be safely used in shell
|
||||||
|
for (const env of resolved) {
|
||||||
|
expect(typeof env).toBe("string");
|
||||||
|
expect(env.length).toBeGreaterThan(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles environment variables with newlines", () => {
|
||||||
|
const serviceEnv = `
|
||||||
|
MULTILINE="line1
|
||||||
|
line2
|
||||||
|
line3"
|
||||||
|
`;
|
||||||
|
|
||||||
|
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
|
||||||
|
|
||||||
|
expect(resolved.length).toBe(1);
|
||||||
|
expect(resolved[0]).toContain("MULTILINE");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles empty environment variable values", () => {
|
||||||
|
const serviceEnv = `
|
||||||
|
EMPTY=
|
||||||
|
EMPTY_QUOTED=""
|
||||||
|
EMPTY_SINGLE=''
|
||||||
|
`;
|
||||||
|
|
||||||
|
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
|
||||||
|
|
||||||
|
// shell-quote escapes the = sign for empty values
|
||||||
|
expect(resolved).toEqual([
|
||||||
|
"EMPTY\\=",
|
||||||
|
"EMPTY_QUOTED\\=",
|
||||||
|
"EMPTY_SINGLE\\=",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles environment variables with equals signs in values", () => {
|
||||||
|
const serviceEnv = `
|
||||||
|
EQUATION=a=b+c
|
||||||
|
CONNECTION_STRING=user=admin;password=test
|
||||||
|
`;
|
||||||
|
|
||||||
|
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
|
||||||
|
|
||||||
|
expect(resolved.length).toBe(2);
|
||||||
|
expect(resolved[0]).toContain("EQUATION");
|
||||||
|
expect(resolved[1]).toContain("CONNECTION_STRING");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves and escapes environment variables together", () => {
|
||||||
|
const projectEnv = `
|
||||||
|
BASE_URL=https://example.com
|
||||||
|
API_KEY='secret-key-with-quotes'
|
||||||
|
`;
|
||||||
|
|
||||||
|
const environmentEnv = `
|
||||||
|
ENV_NAME=production
|
||||||
|
DB_PASS='pa$$word'
|
||||||
|
`;
|
||||||
|
|
||||||
|
const serviceEnv = `
|
||||||
|
FULL_URL=\${{project.BASE_URL}}/api
|
||||||
|
AUTH_KEY=\${{project.API_KEY}}
|
||||||
|
ENVIRONMENT=\${{environment.ENV_NAME}}
|
||||||
|
DB_PASSWORD=\${{environment.DB_PASS}}
|
||||||
|
CUSTOM='value with 'quotes' inside'
|
||||||
|
`;
|
||||||
|
|
||||||
|
const resolved = prepareEnvironmentVariablesForShell(
|
||||||
|
serviceEnv,
|
||||||
|
projectEnv,
|
||||||
|
environmentEnv,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(resolved.length).toBe(5);
|
||||||
|
// All resolved values should be properly escaped
|
||||||
|
for (const env of resolved) {
|
||||||
|
expect(typeof env).toBe("string");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles environment variables with semicolons and ampersands", () => {
|
||||||
|
const serviceEnv = `
|
||||||
|
COMMAND=echo "test" && echo "test2"
|
||||||
|
MULTIPLE=cmd1; cmd2; cmd3
|
||||||
|
URL_WITH_PARAMS=https://example.com?a=1&b=2&c=3
|
||||||
|
`;
|
||||||
|
|
||||||
|
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
|
||||||
|
|
||||||
|
expect(resolved.length).toBe(3);
|
||||||
|
// These should be safely escaped to prevent command injection
|
||||||
|
for (const env of resolved) {
|
||||||
|
expect(typeof env).toBe("string");
|
||||||
|
expect(env.length).toBeGreaterThan(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles environment variables with pipes and redirects", () => {
|
||||||
|
const serviceEnv = `
|
||||||
|
PIPE_COMMAND=cat file | grep test
|
||||||
|
REDIRECT=echo "test" > output.txt
|
||||||
|
BOTH=cat input.txt | grep pattern > output.txt
|
||||||
|
`;
|
||||||
|
|
||||||
|
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
|
||||||
|
|
||||||
|
expect(resolved.length).toBe(3);
|
||||||
|
// Pipes and redirects should be safely quoted
|
||||||
|
expect(resolved[0]).toContain("PIPE_COMMAND");
|
||||||
|
expect(resolved[1]).toContain("REDIRECT");
|
||||||
|
expect(resolved[2]).toContain("BOTH");
|
||||||
|
// At least one should contain a pipe
|
||||||
|
const hasPipe = resolved.some((env) => env.includes("|"));
|
||||||
|
expect(hasPipe).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles environment variables with parentheses and brackets", () => {
|
||||||
|
const serviceEnv = `
|
||||||
|
MATH=(a+b)*c
|
||||||
|
ARRAY=[1,2,3]
|
||||||
|
JSON={"key":"value"}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
|
||||||
|
|
||||||
|
expect(resolved.length).toBe(3);
|
||||||
|
expect(resolved[0]).toContain("(");
|
||||||
|
expect(resolved[1]).toContain("[");
|
||||||
|
expect(resolved[2]).toContain("{");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles very long environment variable values", () => {
|
||||||
|
const longValue = "a".repeat(10000);
|
||||||
|
const serviceEnv = `LONG_VAR=${longValue}`;
|
||||||
|
|
||||||
|
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
|
||||||
|
|
||||||
|
expect(resolved.length).toBe(1);
|
||||||
|
expect(resolved[0]).toContain("LONG_VAR");
|
||||||
|
expect(resolved[0]?.length).toBeGreaterThan(10000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles special unicode characters in environment variables", () => {
|
||||||
|
const serviceEnv = `
|
||||||
|
EMOJI=Hello 🌍 World 🚀
|
||||||
|
CHINESE=你好世界
|
||||||
|
SPECIAL=café résumé naïve
|
||||||
|
`;
|
||||||
|
|
||||||
|
const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", "");
|
||||||
|
|
||||||
|
expect(resolved.length).toBe(3);
|
||||||
|
expect(resolved[0]).toContain("🌍");
|
||||||
|
expect(resolved[1]).toContain("你好");
|
||||||
|
expect(resolved[2]).toContain("café");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -150,8 +150,8 @@ export const AddApplication = ({ environmentId, projectName }: Props) => {
|
|||||||
placeholder="Frontend"
|
placeholder="Frontend"
|
||||||
{...field}
|
{...field}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const val = e.target.value?.trim() || "";
|
const val = e.target.value || "";
|
||||||
const serviceName = slugify(val);
|
const serviceName = slugify(val.trim());
|
||||||
form.setValue("appName", `${slug}-${serviceName}`);
|
form.setValue("appName", `${slug}-${serviceName}`);
|
||||||
field.onChange(val);
|
field.onChange(val);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -161,8 +161,8 @@ export const AddCompose = ({ environmentId, projectName }: Props) => {
|
|||||||
placeholder="Frontend"
|
placeholder="Frontend"
|
||||||
{...field}
|
{...field}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const val = e.target.value?.trim() || "";
|
const val = e.target.value || "";
|
||||||
const serviceName = slugify(val);
|
const serviceName = slugify(val.trim());
|
||||||
form.setValue("appName", `${slug}-${serviceName}`);
|
form.setValue("appName", `${slug}-${serviceName}`);
|
||||||
field.onChange(val);
|
field.onChange(val);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -395,8 +395,8 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
|||||||
placeholder="Name"
|
placeholder="Name"
|
||||||
{...field}
|
{...field}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const val = e.target.value?.trim() || "";
|
const val = e.target.value || "";
|
||||||
const serviceName = slugify(val);
|
const serviceName = slugify(val.trim());
|
||||||
form.setValue("appName", `${slug}-${serviceName}`);
|
form.setValue("appName", `${slug}-${serviceName}`);
|
||||||
field.onChange(val);
|
field.onChange(val);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1261,8 +1261,10 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
toast.success("Connection Success");
|
toast.success("Connection Success");
|
||||||
} catch {
|
} catch (error) {
|
||||||
toast.error("Error testing the provider");
|
toast.error(
|
||||||
|
`Error testing the provider ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCaption,
|
|
||||||
TableCell,
|
TableCell,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
@@ -68,7 +67,6 @@ export const ShowUsers = () => {
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||||
<Table>
|
<Table>
|
||||||
<TableCaption>See all users</TableCaption>
|
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-[100px]">Email</TableHead>
|
<TableHead className="w-[100px]">Email</TableHead>
|
||||||
@@ -111,35 +109,75 @@ export const ShowUsers = () => {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell className="text-right flex justify-end">
|
<TableCell className="text-right flex justify-end">
|
||||||
<DropdownMenu>
|
{member.role !== "owner" && (
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenu>
|
||||||
<Button
|
<DropdownMenuTrigger asChild>
|
||||||
variant="ghost"
|
<Button
|
||||||
className="h-8 w-8 p-0"
|
variant="ghost"
|
||||||
>
|
className="h-8 w-8 p-0"
|
||||||
<span className="sr-only">Open menu</span>
|
>
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
<span className="sr-only">
|
||||||
</Button>
|
Open menu
|
||||||
</DropdownMenuTrigger>
|
</span>
|
||||||
<DropdownMenuContent align="end">
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
<DropdownMenuLabel>
|
</Button>
|
||||||
Actions
|
</DropdownMenuTrigger>
|
||||||
</DropdownMenuLabel>
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuLabel>
|
||||||
|
Actions
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
|
||||||
{member.role !== "owner" && (
|
|
||||||
<AddUserPermissions
|
<AddUserPermissions
|
||||||
userId={member.user.id}
|
userId={member.user.id}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
|
|
||||||
{member.role !== "owner" && (
|
{!isCloud && (
|
||||||
<>
|
<DialogAction
|
||||||
{!isCloud && (
|
title="Delete User"
|
||||||
<DialogAction
|
description="Are you sure you want to delete this user?"
|
||||||
title="Delete User"
|
type="destructive"
|
||||||
description="Are you sure you want to delete this user?"
|
onClick={async () => {
|
||||||
type="destructive"
|
await mutateAsync({
|
||||||
onClick={async () => {
|
userId: member.user.id,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success(
|
||||||
|
"User deleted successfully",
|
||||||
|
);
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error(
|
||||||
|
"Error deleting destination",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
Delete User
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DialogAction>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DialogAction
|
||||||
|
title="Unlink User"
|
||||||
|
description="Are you sure you want to unlink this user?"
|
||||||
|
type="destructive"
|
||||||
|
onClick={async () => {
|
||||||
|
if (!isCloud) {
|
||||||
|
const orgCount =
|
||||||
|
await utils.user.checkUserOrganizations.fetch(
|
||||||
|
{
|
||||||
|
userId: member.user.id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(orgCount);
|
||||||
|
|
||||||
|
if (orgCount === 1) {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
userId: member.user.id,
|
userId: member.user.id,
|
||||||
})
|
})
|
||||||
@@ -151,86 +189,40 @@ export const ShowUsers = () => {
|
|||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error(
|
toast.error(
|
||||||
"Error deleting destination",
|
"Error deleting user",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}}
|
return;
|
||||||
>
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
|
|
||||||
onSelect={(e) =>
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Delete User
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DialogAction>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<DialogAction
|
|
||||||
title="Unlink User"
|
|
||||||
description="Are you sure you want to unlink this user?"
|
|
||||||
type="destructive"
|
|
||||||
onClick={async () => {
|
|
||||||
if (!isCloud) {
|
|
||||||
const orgCount =
|
|
||||||
await utils.user.checkUserOrganizations.fetch(
|
|
||||||
{
|
|
||||||
userId: member.user.id,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(orgCount);
|
|
||||||
|
|
||||||
if (orgCount === 1) {
|
|
||||||
await mutateAsync({
|
|
||||||
userId: member.user.id,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success(
|
|
||||||
"User deleted successfully",
|
|
||||||
);
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error(
|
|
||||||
"Error deleting user",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const { error } =
|
const { error } =
|
||||||
await authClient.organization.removeMember(
|
await authClient.organization.removeMember(
|
||||||
{
|
{
|
||||||
memberIdOrEmail: member.id,
|
memberIdOrEmail: member.id,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!error) {
|
if (!error) {
|
||||||
toast.success(
|
toast.success(
|
||||||
"User unlinked successfully",
|
"User unlinked successfully",
|
||||||
);
|
);
|
||||||
refetch();
|
refetch();
|
||||||
} else {
|
} else {
|
||||||
toast.error(
|
toast.error("Error unlinking user");
|
||||||
"Error unlinking user",
|
}
|
||||||
);
|
}}
|
||||||
}
|
>
|
||||||
}}
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
>
|
>
|
||||||
<DropdownMenuItem
|
Unlink User
|
||||||
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
|
</DropdownMenuItem>
|
||||||
onSelect={(e) => e.preventDefault()}
|
</DialogAction>
|
||||||
>
|
</DropdownMenuContent>
|
||||||
Unlink User
|
</DropdownMenu>
|
||||||
</DropdownMenuItem>
|
)}
|
||||||
</DialogAction>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -44,14 +44,20 @@ export function TimeBadge() {
|
|||||||
.padStart(2, "0")}`;
|
.padStart(2, "0")}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formattedTime = new Intl.DateTimeFormat("en-US", {
|
||||||
|
timeZone: serverTime.timezone,
|
||||||
|
timeStyle: "medium",
|
||||||
|
hour12: false,
|
||||||
|
}).format(time);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="inline-flex items-center gap-2 rounded-md border px-2 py-1 text-xs sm:text-sm whitespace-nowrap max-w-full overflow-hidden">
|
<div className="inline-flex items-center rounded-full border p-1 text-xs whitespace-nowrap max-w-full overflow-hidden gap-1">
|
||||||
<span className="hidden sm:inline">Server Time:</span>
|
<div className="inline-flex items-center px-1 gap-1">
|
||||||
<span className="font-medium tabular-nums">
|
<span className="hidden sm:inline">Server Time:</span>
|
||||||
{time.toLocaleTimeString()}
|
<span className="font-medium tabular-nums">{formattedTime}</span>
|
||||||
</span>
|
</div>
|
||||||
<span className="hidden sm:inline text-muted-foreground">
|
<span className="hidden sm:inline text-primary/70 border rounded-full bg-foreground/5 px-1.5 py-0.5">
|
||||||
({serverTime.timezone} | {getUtcOffset(serverTime.timezone)})
|
{serverTime.timezone} | {getUtcOffset(serverTime.timezone)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
9
apps/dokploy/drizzle/0121_rainy_cargill.sql
Normal file
9
apps/dokploy/drizzle/0121_rainy_cargill.sql
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
-- Fix inconsistent date formats in environment.createdAt field
|
||||||
|
-- Convert PostgreSQL timestamp format to ISO 8601 format
|
||||||
|
-- This addresses issue #2992 where old environments have PostgreSQL timestamp format
|
||||||
|
-- while new ones have ISO 8601 format
|
||||||
|
|
||||||
|
UPDATE "environment"
|
||||||
|
SET "createdAt" = to_char("createdAt"::timestamptz, 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')
|
||||||
|
WHERE "createdAt" NOT LIKE '%T%';
|
||||||
|
|
||||||
6722
apps/dokploy/drizzle/meta/0121_snapshot.json
Normal file
6722
apps/dokploy/drizzle/meta/0121_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -848,6 +848,13 @@
|
|||||||
"when": 1762632540024,
|
"when": 1762632540024,
|
||||||
"tag": "0120_lame_captain_midlands",
|
"tag": "0120_lame_captain_midlands",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 121,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1763755037033,
|
||||||
|
"tag": "0121_rainy_cargill",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.25.8",
|
"version": "v0.25.11",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -98,6 +98,7 @@
|
|||||||
"bl": "6.0.11",
|
"bl": "6.0.11",
|
||||||
"boxen": "^7.1.1",
|
"boxen": "^7.1.1",
|
||||||
"bullmq": "5.4.2",
|
"bullmq": "5.4.2",
|
||||||
|
"shell-quote": "^1.8.1",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^0.2.1",
|
"cmdk": "^0.2.1",
|
||||||
@@ -157,6 +158,7 @@
|
|||||||
"zod-form-data": "^2.0.7"
|
"zod-form-data": "^2.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/shell-quote": "^1.7.5",
|
||||||
"@types/adm-zip": "^0.5.7",
|
"@types/adm-zip": "^0.5.7",
|
||||||
"@types/bcrypt": "5.0.2",
|
"@types/bcrypt": "5.0.2",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
|
|||||||
@@ -47,15 +47,19 @@ export const destinationRouter = createTRPCRouter({
|
|||||||
input;
|
input;
|
||||||
try {
|
try {
|
||||||
const rcloneFlags = [
|
const rcloneFlags = [
|
||||||
`--s3-access-key-id=${accessKey}`,
|
`--s3-access-key-id="${accessKey}"`,
|
||||||
`--s3-secret-access-key=${secretAccessKey}`,
|
`--s3-secret-access-key="${secretAccessKey}"`,
|
||||||
`--s3-region=${region}`,
|
`--s3-region="${region}"`,
|
||||||
`--s3-endpoint=${endpoint}`,
|
`--s3-endpoint="${endpoint}"`,
|
||||||
"--s3-no-check-bucket",
|
"--s3-no-check-bucket",
|
||||||
"--s3-force-path-style",
|
"--s3-force-path-style",
|
||||||
|
"--retries 1",
|
||||||
|
"--low-level-retries 1",
|
||||||
|
"--timeout 10s",
|
||||||
|
"--contimeout 5s",
|
||||||
];
|
];
|
||||||
if (provider) {
|
if (provider) {
|
||||||
rcloneFlags.unshift(`--s3-provider=${provider}`);
|
rcloneFlags.unshift(`--s3-provider="${provider}"`);
|
||||||
}
|
}
|
||||||
const rcloneDestination = `:s3:${bucket}`;
|
const rcloneDestination = `:s3:${bucket}`;
|
||||||
const rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
|
const rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Error testing the notification",
|
message: `${error instanceof Error ? error.message : "Unknown error"}`,
|
||||||
cause: error,
|
cause: error,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -228,7 +228,7 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Error testing the notification",
|
message: `${error instanceof Error ? error.message : "Unknown error"}`,
|
||||||
cause: error,
|
cause: error,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -285,7 +285,7 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Error testing the notification",
|
message: `${error instanceof Error ? error.message : "Unknown error"}`,
|
||||||
cause: error,
|
cause: error,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
findEnvironmentById,
|
findEnvironmentById,
|
||||||
findPostgresById,
|
findPostgresById,
|
||||||
findProjectById,
|
findProjectById,
|
||||||
|
getMountPath,
|
||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
rebuildDatabase,
|
rebuildDatabase,
|
||||||
removePostgresById,
|
removePostgresById,
|
||||||
@@ -37,6 +38,7 @@ import {
|
|||||||
postgres as postgresTable,
|
postgres as postgresTable,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import { cancelJobs } from "@/server/utils/backup";
|
import { cancelJobs } from "@/server/utils/backup";
|
||||||
|
|
||||||
export const postgresRouter = createTRPCRouter({
|
export const postgresRouter = createTRPCRouter({
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
.input(apiCreatePostgres)
|
.input(apiCreatePostgres)
|
||||||
@@ -79,11 +81,13 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mountPath = getMountPath(input.dockerImage);
|
||||||
|
|
||||||
await createMount({
|
await createMount({
|
||||||
serviceId: newPostgres.postgresId,
|
serviceId: newPostgres.postgresId,
|
||||||
serviceType: "postgres",
|
serviceType: "postgres",
|
||||||
volumeName: `${newPostgres.appName}-data`,
|
volumeName: `${newPostgres.appName}-data`,
|
||||||
mountPath: "/var/lib/postgresql/data",
|
mountPath: mountPath,
|
||||||
type: "volume",
|
type: "volume",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -282,12 +286,16 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
const backups = await findBackupsByDbId(input.postgresId, "postgres");
|
const backups = await findBackupsByDbId(input.postgresId, "postgres");
|
||||||
|
|
||||||
const cleanupOperations = [
|
const cleanupOperations = [
|
||||||
removeService(postgres.appName, postgres.serverId),
|
async () => await removeService(postgres?.appName, postgres.serverId),
|
||||||
cancelJobs(backups),
|
async () => await cancelJobs(backups),
|
||||||
removePostgresById(input.postgresId),
|
async () => await removePostgresById(input.postgresId),
|
||||||
];
|
];
|
||||||
|
|
||||||
await Promise.allSettled(cleanupOperations);
|
for (const operation of cleanupOperations) {
|
||||||
|
try {
|
||||||
|
await operation();
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
return postgres;
|
return postgres;
|
||||||
}),
|
}),
|
||||||
@@ -363,6 +371,7 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
message: "You are not authorized to update this Postgres",
|
message: "You are not authorized to update this Postgres",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const service = await updatePostgresById(postgresId, {
|
const service = await updatePostgresById(postgresId, {
|
||||||
...rest,
|
...rest,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -587,7 +587,7 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
return ports.some((port) => port.targetPort === 8080);
|
return ports.some((port) => port.targetPort === 8080);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
readStatsLogs: adminProcedure
|
readStatsLogs: protectedProcedure
|
||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
path: "/read-stats-logs",
|
path: "/read-stats-logs",
|
||||||
@@ -650,7 +650,7 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
const processedLogs = processLogs(rawConfig as string, input?.dateRange);
|
const processedLogs = processLogs(rawConfig as string, input?.dateRange);
|
||||||
return processedLogs || [];
|
return processedLogs || [];
|
||||||
}),
|
}),
|
||||||
haveActivateRequests: adminProcedure.query(async () => {
|
haveActivateRequests: protectedProcedure.query(async () => {
|
||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -665,7 +665,7 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return !!parsedConfig?.accessLog?.filePath;
|
return !!parsedConfig?.accessLog?.filePath;
|
||||||
}),
|
}),
|
||||||
toggleRequests: adminProcedure
|
toggleRequests: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
enable: z.boolean(),
|
enable: z.boolean(),
|
||||||
@@ -835,7 +835,7 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
const ports = await readPorts("dokploy-traefik", input?.serverId);
|
const ports = await readPorts("dokploy-traefik", input?.serverId);
|
||||||
return ports;
|
return ports;
|
||||||
}),
|
}),
|
||||||
updateLogCleanup: adminProcedure
|
updateLogCleanup: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
cronExpression: z.string().nullable(),
|
cronExpression: z.string().nullable(),
|
||||||
@@ -851,7 +851,7 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
return stopLogCleanup();
|
return stopLogCleanup();
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getLogCleanupStatus: adminProcedure.query(async () => {
|
getLogCleanupStatus: protectedProcedure.query(async () => {
|
||||||
return getLogCleanupStatus();
|
return getLogCleanupStatus();
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,7 @@
|
|||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"rotating-file-stream": "3.2.3",
|
"rotating-file-stream": "3.2.3",
|
||||||
|
"shell-quote": "^1.8.1",
|
||||||
"slugify": "^1.6.6",
|
"slugify": "^1.6.6",
|
||||||
"ssh2": "1.15.0",
|
"ssh2": "1.15.0",
|
||||||
"toml": "3.0.0",
|
"toml": "3.0.0",
|
||||||
@@ -93,6 +94,7 @@
|
|||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"@types/react": "^18.3.5",
|
"@types/react": "^18.3.5",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"@types/shell-quote": "^1.7.5",
|
||||||
"@types/ssh2": "1.15.1",
|
"@types/ssh2": "1.15.1",
|
||||||
"@types/ws": "8.5.10",
|
"@types/ws": "8.5.10",
|
||||||
"drizzle-kit": "^0.30.6",
|
"drizzle-kit": "^0.30.6",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export type TemplateProps = {
|
|||||||
applicationType: string;
|
applicationType: string;
|
||||||
buildLink: string;
|
buildLink: string;
|
||||||
date: string;
|
date: string;
|
||||||
|
environmentName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BuildSuccessEmail = ({
|
export const BuildSuccessEmail = ({
|
||||||
@@ -27,6 +28,7 @@ export const BuildSuccessEmail = ({
|
|||||||
applicationType = "application",
|
applicationType = "application",
|
||||||
buildLink = "https://dokploy.com/projects/dokploy-test/applications/dokploy-test",
|
buildLink = "https://dokploy.com/projects/dokploy-test/applications/dokploy-test",
|
||||||
date = "2023-05-01T00:00:00.000Z",
|
date = "2023-05-01T00:00:00.000Z",
|
||||||
|
environmentName = "production",
|
||||||
}: TemplateProps) => {
|
}: TemplateProps) => {
|
||||||
const previewText = `Build success for ${applicationName}`;
|
const previewText = `Build success for ${applicationName}`;
|
||||||
return (
|
return (
|
||||||
@@ -74,6 +76,9 @@ export const BuildSuccessEmail = ({
|
|||||||
<Text className="!leading-3">
|
<Text className="!leading-3">
|
||||||
Application Name: <strong>{applicationName}</strong>
|
Application Name: <strong>{applicationName}</strong>
|
||||||
</Text>
|
</Text>
|
||||||
|
<Text className="!leading-3">
|
||||||
|
Environment: <strong>{environmentName}</strong>
|
||||||
|
</Text>
|
||||||
<Text className="!leading-3">
|
<Text className="!leading-3">
|
||||||
Application Type: <strong>{applicationType}</strong>
|
Application Type: <strong>{applicationType}</strong>
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
import { sendBuildErrorNotifications } from "@dokploy/server/utils/notifications/build-error";
|
import { sendBuildErrorNotifications } from "@dokploy/server/utils/notifications/build-error";
|
||||||
import { sendBuildSuccessNotifications } from "@dokploy/server/utils/notifications/build-success";
|
import { sendBuildSuccessNotifications } from "@dokploy/server/utils/notifications/build-success";
|
||||||
import {
|
import {
|
||||||
|
ExecError,
|
||||||
execAsync,
|
execAsync,
|
||||||
execAsyncRemote,
|
execAsyncRemote,
|
||||||
} from "@dokploy/server/utils/process/execAsync";
|
} from "@dokploy/server/utils/process/execAsync";
|
||||||
@@ -28,6 +29,7 @@ import { cloneGitlabRepository } from "@dokploy/server/utils/providers/gitlab";
|
|||||||
import { createTraefikConfig } from "@dokploy/server/utils/traefik/application";
|
import { createTraefikConfig } from "@dokploy/server/utils/traefik/application";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
|
import { encodeBase64 } from "../utils/docker/utils";
|
||||||
import { getDokployUrl } from "./admin";
|
import { getDokployUrl } from "./admin";
|
||||||
import {
|
import {
|
||||||
createDeployment,
|
createDeployment,
|
||||||
@@ -225,9 +227,19 @@ export const deployApplication = async ({
|
|||||||
buildLink,
|
buildLink,
|
||||||
organizationId: application.environment.project.organizationId,
|
organizationId: application.environment.project.organizationId,
|
||||||
domains: application.domains,
|
domains: application.domains,
|
||||||
|
environmentName: application.environment.name,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const command = `echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath};`;
|
let command = "";
|
||||||
|
|
||||||
|
// Only log details for non-ExecError errors
|
||||||
|
if (!(error instanceof ExecError)) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
const encodedMessage = encodeBase64(message);
|
||||||
|
command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`;
|
||||||
|
}
|
||||||
|
|
||||||
|
command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`;
|
||||||
if (application.serverId) {
|
if (application.serverId) {
|
||||||
await execAsyncRemote(application.serverId, command);
|
await execAsyncRemote(application.serverId, command);
|
||||||
} else {
|
} else {
|
||||||
@@ -273,6 +285,7 @@ export const rebuildApplication = async ({
|
|||||||
descriptionLog: string;
|
descriptionLog: string;
|
||||||
}) => {
|
}) => {
|
||||||
const application = await findApplicationById(applicationId);
|
const application = await findApplicationById(applicationId);
|
||||||
|
const buildLink = `${await getDokployUrl()}/dashboard/project/${application.environment.projectId}/environment/${application.environmentId}/services/application/${application.applicationId}?tab=deployments`;
|
||||||
|
|
||||||
const deployment = await createDeployment({
|
const deployment = await createDeployment({
|
||||||
applicationId: applicationId,
|
applicationId: applicationId,
|
||||||
@@ -293,7 +306,43 @@ export const rebuildApplication = async ({
|
|||||||
await mechanizeDockerContainer(application);
|
await mechanizeDockerContainer(application);
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||||
await updateApplicationStatus(applicationId, "done");
|
await updateApplicationStatus(applicationId, "done");
|
||||||
|
|
||||||
|
if (application.rollbackActive) {
|
||||||
|
const tagImage =
|
||||||
|
application.sourceType === "docker"
|
||||||
|
? application.dockerImage
|
||||||
|
: application.appName;
|
||||||
|
await createRollback({
|
||||||
|
appName: tagImage || "",
|
||||||
|
deploymentId: deployment.deploymentId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendBuildSuccessNotifications({
|
||||||
|
projectName: application.environment.project.name,
|
||||||
|
applicationName: application.name,
|
||||||
|
applicationType: "application",
|
||||||
|
buildLink,
|
||||||
|
organizationId: application.environment.project.organizationId,
|
||||||
|
domains: application.domains,
|
||||||
|
environmentName: application.environment.name,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
let command = "";
|
||||||
|
|
||||||
|
// Only log details for non-ExecError errors
|
||||||
|
if (!(error instanceof ExecError)) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
const encodedMessage = encodeBase64(message);
|
||||||
|
command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`;
|
||||||
|
}
|
||||||
|
|
||||||
|
command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`;
|
||||||
|
if (application.serverId) {
|
||||||
|
await execAsyncRemote(application.serverId, command);
|
||||||
|
} else {
|
||||||
|
await execAsync(command);
|
||||||
|
}
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||||
await updateApplicationStatus(applicationId, "error");
|
await updateApplicationStatus(applicationId, "error");
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import type { ComposeSpecification } from "@dokploy/server/utils/docker/types";
|
|||||||
import { sendBuildErrorNotifications } from "@dokploy/server/utils/notifications/build-error";
|
import { sendBuildErrorNotifications } from "@dokploy/server/utils/notifications/build-error";
|
||||||
import { sendBuildSuccessNotifications } from "@dokploy/server/utils/notifications/build-success";
|
import { sendBuildSuccessNotifications } from "@dokploy/server/utils/notifications/build-success";
|
||||||
import {
|
import {
|
||||||
|
ExecError,
|
||||||
execAsync,
|
execAsync,
|
||||||
execAsyncRemote,
|
execAsyncRemote,
|
||||||
} from "@dokploy/server/utils/process/execAsync";
|
} from "@dokploy/server/utils/process/execAsync";
|
||||||
@@ -32,6 +33,7 @@ import { cloneGitlabRepository } from "@dokploy/server/utils/providers/gitlab";
|
|||||||
import { getCreateComposeFileCommand } from "@dokploy/server/utils/providers/raw";
|
import { getCreateComposeFileCommand } from "@dokploy/server/utils/providers/raw";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
|
import { encodeBase64 } from "../utils/docker/utils";
|
||||||
import { getDokployUrl } from "./admin";
|
import { getDokployUrl } from "./admin";
|
||||||
import {
|
import {
|
||||||
createDeploymentCompose,
|
createDeploymentCompose,
|
||||||
@@ -267,8 +269,24 @@ export const deployCompose = async ({
|
|||||||
buildLink,
|
buildLink,
|
||||||
organizationId: compose.environment.project.organizationId,
|
organizationId: compose.environment.project.organizationId,
|
||||||
domains: compose.domains,
|
domains: compose.domains,
|
||||||
|
environmentName: compose.environment.name,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
let command = "";
|
||||||
|
|
||||||
|
// Only log details for non-ExecError errors
|
||||||
|
if (!(error instanceof ExecError)) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
const encodedMessage = encodeBase64(message);
|
||||||
|
command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`;
|
||||||
|
}
|
||||||
|
|
||||||
|
command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`;
|
||||||
|
if (compose.serverId) {
|
||||||
|
await execAsyncRemote(compose.serverId, command);
|
||||||
|
} else {
|
||||||
|
await execAsync(command);
|
||||||
|
}
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||||
await updateCompose(composeId, {
|
await updateCompose(composeId, {
|
||||||
composeStatus: "error",
|
composeStatus: "error",
|
||||||
@@ -341,6 +359,21 @@ export const rebuildCompose = async ({
|
|||||||
composeStatus: "done",
|
composeStatus: "done",
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
let command = "";
|
||||||
|
|
||||||
|
// Only log details for non-ExecError errors
|
||||||
|
if (!(error instanceof ExecError)) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
const encodedMessage = encodeBase64(message);
|
||||||
|
command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`;
|
||||||
|
}
|
||||||
|
|
||||||
|
command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`;
|
||||||
|
if (compose.serverId) {
|
||||||
|
await execAsyncRemote(compose.serverId, command);
|
||||||
|
} else {
|
||||||
|
await execAsync(command);
|
||||||
|
}
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||||
await updateCompose(composeId, {
|
await updateCompose(composeId, {
|
||||||
composeStatus: "error",
|
composeStatus: "error",
|
||||||
@@ -375,7 +408,7 @@ export const removeCompose = async (
|
|||||||
} else {
|
} else {
|
||||||
const command = `
|
const command = `
|
||||||
docker network disconnect ${compose.appName} dokploy-traefik;
|
docker network disconnect ${compose.appName} dokploy-traefik;
|
||||||
cd ${projectPath} && docker compose -p ${compose.appName} down ${
|
cd ${projectPath} && env -i PATH="$PATH" docker compose -p ${compose.appName} down ${
|
||||||
deleteVolumes ? "--volumes" : ""
|
deleteVolumes ? "--volumes" : ""
|
||||||
} && rm -rf ${projectPath}`;
|
} && rm -rf ${projectPath}`;
|
||||||
|
|
||||||
@@ -402,7 +435,7 @@ export const startCompose = async (composeId: string) => {
|
|||||||
const projectPath = join(COMPOSE_PATH, compose.appName, "code");
|
const projectPath = join(COMPOSE_PATH, compose.appName, "code");
|
||||||
const path =
|
const path =
|
||||||
compose.sourceType === "raw" ? "docker-compose.yml" : compose.composePath;
|
compose.sourceType === "raw" ? "docker-compose.yml" : compose.composePath;
|
||||||
const baseCommand = `docker compose -p ${compose.appName} -f ${path} up -d`;
|
const baseCommand = `env -i PATH="$PATH" docker compose -p ${compose.appName} -f ${path} up -d`;
|
||||||
if (compose.composeType === "docker-compose") {
|
if (compose.composeType === "docker-compose") {
|
||||||
if (compose.serverId) {
|
if (compose.serverId) {
|
||||||
await execAsyncRemote(
|
await execAsyncRemote(
|
||||||
@@ -437,14 +470,17 @@ export const stopCompose = async (composeId: string) => {
|
|||||||
if (compose.serverId) {
|
if (compose.serverId) {
|
||||||
await execAsyncRemote(
|
await execAsyncRemote(
|
||||||
compose.serverId,
|
compose.serverId,
|
||||||
`cd ${join(COMPOSE_PATH, compose.appName)} && docker compose -p ${
|
`cd ${join(COMPOSE_PATH, compose.appName)} && env -i PATH="$PATH" docker compose -p ${
|
||||||
compose.appName
|
compose.appName
|
||||||
} stop`,
|
} stop`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await execAsync(`docker compose -p ${compose.appName} stop`, {
|
await execAsync(
|
||||||
cwd: join(COMPOSE_PATH, compose.appName),
|
`env -i PATH="$PATH" docker compose -p ${compose.appName} stop`,
|
||||||
});
|
{
|
||||||
|
cwd: join(COMPOSE_PATH, compose.appName),
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,18 @@ import { TRPCError } from "@trpc/server";
|
|||||||
import { eq, getTableColumns } from "drizzle-orm";
|
import { eq, getTableColumns } from "drizzle-orm";
|
||||||
import { validUniqueServerAppName } from "./project";
|
import { validUniqueServerAppName } from "./project";
|
||||||
|
|
||||||
|
export function getMountPath(dockerImage: string): string {
|
||||||
|
const versionMatch = dockerImage.match(/postgres:(\d+)/);
|
||||||
|
|
||||||
|
if (versionMatch?.[1]) {
|
||||||
|
const version = Number.parseInt(versionMatch[1], 10);
|
||||||
|
if (version >= 18) {
|
||||||
|
return `/var/lib/postgresql/${version}/data`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "/var/lib/postgresql/data";
|
||||||
|
}
|
||||||
|
|
||||||
export type Postgres = typeof postgres.$inferSelect;
|
export type Postgres = typeof postgres.$inferSelect;
|
||||||
|
|
||||||
export const createPostgres = async (input: typeof apiCreatePostgres._type) => {
|
export const createPostgres = async (input: typeof apiCreatePostgres._type) => {
|
||||||
|
|||||||
@@ -60,10 +60,7 @@ export const getUpdateData = async (): Promise<IUpdateData> => {
|
|||||||
try {
|
try {
|
||||||
currentDigest = await getServiceImageDigest();
|
currentDigest = await getServiceImageDigest();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
// TODO: Docker versions 29.0.0 change the way to get the service image digest, so we need to update this in the future we upgrade to that version.
|
||||||
// Docker service might not exist locally
|
|
||||||
// You can run the # Installation command for docker service create mentioned in the below docs to test it locally:
|
|
||||||
// https://docs.dokploy.com/docs/core/manual-installation
|
|
||||||
return DEFAULT_UPDATE_DATA;
|
return DEFAULT_UPDATE_DATA;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export const initializePostgres = async () => {
|
|||||||
Mounts: [
|
Mounts: [
|
||||||
{
|
{
|
||||||
Type: "volume",
|
Type: "volume",
|
||||||
Source: "dokploy-postgres-database",
|
Source: "dokploy-postgres",
|
||||||
Target: "/var/lib/postgresql/data",
|
Target: "/var/lib/postgresql/data",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const initializeRedis = async () => {
|
|||||||
Mounts: [
|
Mounts: [
|
||||||
{
|
{
|
||||||
Type: "volume",
|
Type: "volume",
|
||||||
Source: "redis-data-volume",
|
Source: "dokploy-redis",
|
||||||
Target: "/data",
|
Target: "/data",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { dirname, join } from "node:path";
|
|||||||
import { paths } from "@dokploy/server/constants";
|
import { paths } from "@dokploy/server/constants";
|
||||||
import type { InferResultType } from "@dokploy/server/types/with";
|
import type { InferResultType } from "@dokploy/server/types/with";
|
||||||
import boxen from "boxen";
|
import boxen from "boxen";
|
||||||
|
import { quote } from "shell-quote";
|
||||||
import { writeDomainsToCompose } from "../docker/domain";
|
import { writeDomainsToCompose } from "../docker/domain";
|
||||||
import {
|
import {
|
||||||
encodeBase64,
|
encodeBase64,
|
||||||
@@ -52,9 +53,8 @@ Compose Type: ${composeType} ✅`;
|
|||||||
|
|
||||||
cd "${projectPath}";
|
cd "${projectPath}";
|
||||||
|
|
||||||
${exportEnvCommand}
|
|
||||||
${compose.isolatedDeployment ? `docker network inspect ${compose.appName} >/dev/null 2>&1 || docker network create --attachable ${compose.appName}` : ""}
|
${compose.isolatedDeployment ? `docker network inspect ${compose.appName} >/dev/null 2>&1 || docker network create --attachable ${compose.appName}` : ""}
|
||||||
docker ${command.split(" ").join(" ")} 2>&1 || { echo "Error: ❌ Docker command failed"; exit 1; }
|
env -i PATH="$PATH" ${exportEnvCommand} docker ${command.split(" ").join(" ")} 2>&1 || { echo "Error: ❌ Docker command failed"; exit 1; }
|
||||||
${compose.isolatedDeployment ? `docker network connect ${compose.appName} $(docker ps --filter "name=dokploy-traefik" -q) >/dev/null 2>&1` : ""}
|
${compose.isolatedDeployment ? `docker network connect ${compose.appName} $(docker ps --filter "name=dokploy-traefik" -q) >/dev/null 2>&1` : ""}
|
||||||
|
|
||||||
echo "Docker Compose Deployed: ✅";
|
echo "Docker Compose Deployed: ✅";
|
||||||
@@ -65,7 +65,6 @@ Compose Type: ${composeType} ✅`;
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
return bashCommand;
|
return bashCommand;
|
||||||
// return await execAsyncRemote(compose.serverId, bashCommand);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const sanitizeCommand = (command: string) => {
|
const sanitizeCommand = (command: string) => {
|
||||||
@@ -137,8 +136,8 @@ const getExportEnvCommand = (compose: ComposeNested) => {
|
|||||||
compose.environment.project.env,
|
compose.environment.project.env,
|
||||||
);
|
);
|
||||||
const exports = Object.entries(envVars)
|
const exports = Object.entries(envVars)
|
||||||
.map(([key, value]) => `export ${key}=${JSON.stringify(value)}`)
|
.map(([key, value]) => `${key}=${quote([value])}`)
|
||||||
.join("\n");
|
.join(" ");
|
||||||
|
|
||||||
return exports ? `\n# Export environment variables\n${exports}\n` : "";
|
return exports ? `${exports}` : "";
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
getEnviromentVariablesObject,
|
getEnviromentVariablesObject,
|
||||||
prepareEnvironmentVariables,
|
prepareEnvironmentVariablesForShell,
|
||||||
} from "@dokploy/server/utils/docker/utils";
|
} from "@dokploy/server/utils/docker/utils";
|
||||||
|
import { quote } from "shell-quote";
|
||||||
import {
|
import {
|
||||||
getBuildAppDirectory,
|
getBuildAppDirectory,
|
||||||
getDockerContextPath,
|
getDockerContextPath,
|
||||||
@@ -40,14 +41,14 @@ export const getDockerCommand = (application: ApplicationNested) => {
|
|||||||
commandArgs.push("--no-cache");
|
commandArgs.push("--no-cache");
|
||||||
}
|
}
|
||||||
|
|
||||||
const args = prepareEnvironmentVariables(
|
const args = prepareEnvironmentVariablesForShell(
|
||||||
buildArgs,
|
buildArgs,
|
||||||
application.environment.project.env,
|
application.environment.project.env,
|
||||||
application.environment.env,
|
application.environment.env,
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const arg of args) {
|
for (const arg of args) {
|
||||||
commandArgs.push("--build-arg", `'${arg}'`);
|
commandArgs.push("--build-arg", arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
const secrets = getEnviromentVariablesObject(
|
const secrets = getEnviromentVariablesObject(
|
||||||
@@ -57,7 +58,7 @@ export const getDockerCommand = (application: ApplicationNested) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const joinedSecrets = Object.entries(secrets)
|
const joinedSecrets = Object.entries(secrets)
|
||||||
.map(([key, value]) => `${key}='${value.replace(/'/g, "'\"'\"'")}'`)
|
.map(([key, value]) => `${key}=${quote([value])}`)
|
||||||
.join(" ");
|
.join(" ");
|
||||||
|
|
||||||
for (const key in secrets) {
|
for (const key in secrets) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { prepareEnvironmentVariables } from "../docker/utils";
|
import { prepareEnvironmentVariablesForShell } from "../docker/utils";
|
||||||
import { getBuildAppDirectory } from "../filesystem/directory";
|
import { getBuildAppDirectory } from "../filesystem/directory";
|
||||||
import type { ApplicationNested } from ".";
|
import type { ApplicationNested } from ".";
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@ export const getHerokuCommand = (application: ApplicationNested) => {
|
|||||||
const { env, appName, cleanCache } = application;
|
const { env, appName, cleanCache } = application;
|
||||||
|
|
||||||
const buildAppDirectory = getBuildAppDirectory(application);
|
const buildAppDirectory = getBuildAppDirectory(application);
|
||||||
const envVariables = prepareEnvironmentVariables(
|
const envVariables = prepareEnvironmentVariablesForShell(
|
||||||
env,
|
env,
|
||||||
application.environment.project.env,
|
application.environment.project.env,
|
||||||
application.environment.env,
|
application.environment.env,
|
||||||
@@ -26,7 +26,7 @@ export const getHerokuCommand = (application: ApplicationNested) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const env of envVariables) {
|
for (const env of envVariables) {
|
||||||
args.push("--env", `'${env}'`);
|
args.push("--env", env);
|
||||||
}
|
}
|
||||||
|
|
||||||
const command = `pack ${args.join(" ")}`;
|
const command = `pack ${args.join(" ")}`;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { getStaticCommand } from "@dokploy/server/utils/builders/static";
|
import { getStaticCommand } from "@dokploy/server/utils/builders/static";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { prepareEnvironmentVariables } from "../docker/utils";
|
import { prepareEnvironmentVariablesForShell } from "../docker/utils";
|
||||||
import { getBuildAppDirectory } from "../filesystem/directory";
|
import { getBuildAppDirectory } from "../filesystem/directory";
|
||||||
import type { ApplicationNested } from ".";
|
import type { ApplicationNested } from ".";
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ export const getNixpacksCommand = (application: ApplicationNested) => {
|
|||||||
|
|
||||||
const buildAppDirectory = getBuildAppDirectory(application);
|
const buildAppDirectory = getBuildAppDirectory(application);
|
||||||
const buildContainerId = `${appName}-${nanoid(10)}`;
|
const buildContainerId = `${appName}-${nanoid(10)}`;
|
||||||
const envVariables = prepareEnvironmentVariables(
|
const envVariables = prepareEnvironmentVariablesForShell(
|
||||||
env,
|
env,
|
||||||
application.environment.project.env,
|
application.environment.project.env,
|
||||||
application.environment.env,
|
application.environment.env,
|
||||||
@@ -23,7 +23,7 @@ export const getNixpacksCommand = (application: ApplicationNested) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const env of envVariables) {
|
for (const env of envVariables) {
|
||||||
args.push("--env", `'${env}'`);
|
args.push("--env", env);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (publishDirectory) {
|
if (publishDirectory) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { prepareEnvironmentVariables } from "../docker/utils";
|
import { prepareEnvironmentVariablesForShell } from "../docker/utils";
|
||||||
import { getBuildAppDirectory } from "../filesystem/directory";
|
import { getBuildAppDirectory } from "../filesystem/directory";
|
||||||
import type { ApplicationNested } from ".";
|
import type { ApplicationNested } from ".";
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@ export const getPaketoCommand = (application: ApplicationNested) => {
|
|||||||
const { env, appName, cleanCache } = application;
|
const { env, appName, cleanCache } = application;
|
||||||
|
|
||||||
const buildAppDirectory = getBuildAppDirectory(application);
|
const buildAppDirectory = getBuildAppDirectory(application);
|
||||||
const envVariables = prepareEnvironmentVariables(
|
const envVariables = prepareEnvironmentVariablesForShell(
|
||||||
env,
|
env,
|
||||||
application.environment.project.env,
|
application.environment.project.env,
|
||||||
application.environment.env,
|
application.environment.env,
|
||||||
@@ -26,7 +26,7 @@ export const getPaketoCommand = (application: ApplicationNested) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const env of envVariables) {
|
for (const env of envVariables) {
|
||||||
args.push("--env", `'${env}'`);
|
args.push("--env", env);
|
||||||
}
|
}
|
||||||
|
|
||||||
const command = `pack ${args.join(" ")}`;
|
const command = `pack ${args.join(" ")}`;
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { createHash } from "node:crypto";
|
import { createHash } from "node:crypto";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
|
import { quote } from "shell-quote";
|
||||||
import {
|
import {
|
||||||
parseEnvironmentKeyValuePair,
|
parseEnvironmentKeyValuePair,
|
||||||
prepareEnvironmentVariables,
|
prepareEnvironmentVariables,
|
||||||
|
prepareEnvironmentVariablesForShell,
|
||||||
} from "../docker/utils";
|
} from "../docker/utils";
|
||||||
import { getBuildAppDirectory } from "../filesystem/directory";
|
import { getBuildAppDirectory } from "../filesystem/directory";
|
||||||
import type { ApplicationNested } from ".";
|
import type { ApplicationNested } from ".";
|
||||||
@@ -18,7 +20,7 @@ const calculateSecretsHash = (envVariables: string[]): string => {
|
|||||||
export const getRailpackCommand = (application: ApplicationNested) => {
|
export const getRailpackCommand = (application: ApplicationNested) => {
|
||||||
const { env, appName, cleanCache } = application;
|
const { env, appName, cleanCache } = application;
|
||||||
const buildAppDirectory = getBuildAppDirectory(application);
|
const buildAppDirectory = getBuildAppDirectory(application);
|
||||||
const envVariables = prepareEnvironmentVariables(
|
const envVariables = prepareEnvironmentVariablesForShell(
|
||||||
env,
|
env,
|
||||||
application.environment.project.env,
|
application.environment.project.env,
|
||||||
application.environment.env,
|
application.environment.env,
|
||||||
@@ -35,7 +37,7 @@ export const getRailpackCommand = (application: ApplicationNested) => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
for (const env of envVariables) {
|
for (const env of envVariables) {
|
||||||
prepareArgs.push("--env", `'${env}'`);
|
prepareArgs.push("--env", env);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate secrets hash for layer invalidation
|
// Calculate secrets hash for layer invalidation
|
||||||
@@ -63,12 +65,18 @@ export const getRailpackCommand = (application: ApplicationNested) => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
// Add secrets properly formatted
|
// Add secrets properly formatted
|
||||||
|
// Use prepareEnvironmentVariables (without ForShell) to get raw values for parsing
|
||||||
|
const rawEnvVariables = prepareEnvironmentVariables(
|
||||||
|
env,
|
||||||
|
application.environment.project.env,
|
||||||
|
application.environment.env,
|
||||||
|
);
|
||||||
const exportEnvs = [];
|
const exportEnvs = [];
|
||||||
for (const pair of envVariables) {
|
for (const pair of rawEnvVariables) {
|
||||||
const [key, value] = parseEnvironmentKeyValuePair(pair);
|
const [key, value] = parseEnvironmentKeyValuePair(pair);
|
||||||
if (key && value) {
|
if (key && value) {
|
||||||
buildArgs.push("--secret", `id=${key},env=${key}`);
|
buildArgs.push("--secret", `id=${key},env=${key}`);
|
||||||
exportEnvs.push(`export ${key}='${value}'`);
|
exportEnvs.push(`export ${key}=${quote([value])}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,6 +85,9 @@ export const getRailpackCommand = (application: ApplicationNested) => {
|
|||||||
const bashCommand = `
|
const bashCommand = `
|
||||||
|
|
||||||
# Ensure we have a builder with containerd
|
# Ensure we have a builder with containerd
|
||||||
|
|
||||||
|
export RAILPACK_VERSION=${application.railpackVersion}
|
||||||
|
bash -c "$(curl -fsSL https://railpack.com/install.sh)"
|
||||||
docker buildx create --use --name builder-containerd --driver docker-container || true
|
docker buildx create --use --name builder-containerd --driver docker-container || true
|
||||||
docker buildx use builder-containerd
|
docker buildx use builder-containerd
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { docker, paths } from "@dokploy/server/constants";
|
|||||||
import type { Compose } from "@dokploy/server/services/compose";
|
import type { Compose } from "@dokploy/server/services/compose";
|
||||||
import type { ContainerInfo, ResourceRequirements } from "dockerode";
|
import type { ContainerInfo, ResourceRequirements } from "dockerode";
|
||||||
import { parse } from "dotenv";
|
import { parse } from "dotenv";
|
||||||
|
import { quote } from "shell-quote";
|
||||||
import type { ApplicationNested } from "../builders";
|
import type { ApplicationNested } from "../builders";
|
||||||
import type { MariadbNested } from "../databases/mariadb";
|
import type { MariadbNested } from "../databases/mariadb";
|
||||||
import type { MongoNested } from "../databases/mongo";
|
import type { MongoNested } from "../databases/mongo";
|
||||||
@@ -310,6 +311,21 @@ export const prepareEnvironmentVariables = (
|
|||||||
return resolvedVars;
|
return resolvedVars;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const prepareEnvironmentVariablesForShell = (
|
||||||
|
serviceEnv: string | null,
|
||||||
|
projectEnv?: string | null,
|
||||||
|
environmentEnv?: string | null,
|
||||||
|
): string[] => {
|
||||||
|
const envVars = prepareEnvironmentVariables(
|
||||||
|
serviceEnv,
|
||||||
|
projectEnv,
|
||||||
|
environmentEnv,
|
||||||
|
);
|
||||||
|
// Using shell-quote library to properly escape shell arguments
|
||||||
|
// This is the standard way to handle special characters in shell commands
|
||||||
|
return envVars.map((env) => quote([env]));
|
||||||
|
};
|
||||||
|
|
||||||
export const parseEnvironmentKeyValuePair = (
|
export const parseEnvironmentKeyValuePair = (
|
||||||
pair: string,
|
pair: string,
|
||||||
): [string, string] => {
|
): [string, string] => {
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import { and, eq } from "drizzle-orm";
|
|||||||
import {
|
import {
|
||||||
sendDiscordNotification,
|
sendDiscordNotification,
|
||||||
sendEmailNotification,
|
sendEmailNotification,
|
||||||
sendLarkNotification,
|
|
||||||
sendGotifyNotification,
|
sendGotifyNotification,
|
||||||
|
sendLarkNotification,
|
||||||
sendNtfyNotification,
|
sendNtfyNotification,
|
||||||
sendSlackNotification,
|
sendSlackNotification,
|
||||||
sendTelegramNotification,
|
sendTelegramNotification,
|
||||||
@@ -52,279 +52,287 @@ export const sendBuildErrorNotifications = async ({
|
|||||||
for (const notification of notificationList) {
|
for (const notification of notificationList) {
|
||||||
const { email, discord, telegram, slack, gotify, ntfy, lark } =
|
const { email, discord, telegram, slack, gotify, ntfy, lark } =
|
||||||
notification;
|
notification;
|
||||||
if (email) {
|
try {
|
||||||
const template = await renderAsync(
|
if (email) {
|
||||||
BuildFailedEmail({
|
const template = await renderAsync(
|
||||||
projectName,
|
BuildFailedEmail({
|
||||||
applicationName,
|
projectName,
|
||||||
applicationType,
|
applicationName,
|
||||||
errorMessage: errorMessage,
|
applicationType,
|
||||||
buildLink,
|
errorMessage: errorMessage,
|
||||||
date: date.toLocaleString(),
|
buildLink,
|
||||||
}),
|
date: date.toLocaleString(),
|
||||||
).catch();
|
}),
|
||||||
await sendEmailNotification(email, "Build failed for dokploy", template);
|
).catch();
|
||||||
}
|
await sendEmailNotification(
|
||||||
|
email,
|
||||||
|
"Build failed for dokploy",
|
||||||
|
template,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (discord) {
|
if (discord) {
|
||||||
const decorate = (decoration: string, text: string) =>
|
const decorate = (decoration: string, text: string) =>
|
||||||
`${discord.decoration ? decoration : ""} ${text}`.trim();
|
`${discord.decoration ? decoration : ""} ${text}`.trim();
|
||||||
|
|
||||||
const limitCharacter = 800;
|
const limitCharacter = 800;
|
||||||
const truncatedErrorMessage = errorMessage.substring(0, limitCharacter);
|
const truncatedErrorMessage = errorMessage.substring(0, limitCharacter);
|
||||||
await sendDiscordNotification(discord, {
|
await sendDiscordNotification(discord, {
|
||||||
title: decorate(">", "`⚠️` Build Failed"),
|
title: decorate(">", "`⚠️` Build Failed"),
|
||||||
color: 0xed4245,
|
color: 0xed4245,
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: decorate("`🛠️`", "Project"),
|
name: decorate("`🛠️`", "Project"),
|
||||||
value: projectName,
|
value: projectName,
|
||||||
inline: true,
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: decorate("`⚙️`", "Application"),
|
||||||
|
value: applicationName,
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: decorate("`❔`", "Type"),
|
||||||
|
value: applicationType,
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: decorate("`📅`", "Date"),
|
||||||
|
value: `<t:${unixDate}:D>`,
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: decorate("`⌚`", "Time"),
|
||||||
|
value: `<t:${unixDate}:t>`,
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: decorate("`❓`", "Type"),
|
||||||
|
value: "Failed",
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: decorate("`⚠️`", "Error Message"),
|
||||||
|
value: `\`\`\`${truncatedErrorMessage}\`\`\``,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: decorate("`🧷`", "Build Link"),
|
||||||
|
value: `[Click here to access build link](${buildLink})`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
timestamp: date.toISOString(),
|
||||||
|
footer: {
|
||||||
|
text: "Dokploy Build Notification",
|
||||||
},
|
},
|
||||||
{
|
});
|
||||||
name: decorate("`⚙️`", "Application"),
|
}
|
||||||
value: applicationName,
|
|
||||||
inline: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: decorate("`❔`", "Type"),
|
|
||||||
value: applicationType,
|
|
||||||
inline: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: decorate("`📅`", "Date"),
|
|
||||||
value: `<t:${unixDate}:D>`,
|
|
||||||
inline: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: decorate("`⌚`", "Time"),
|
|
||||||
value: `<t:${unixDate}:t>`,
|
|
||||||
inline: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: decorate("`❓`", "Type"),
|
|
||||||
value: "Failed",
|
|
||||||
inline: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: decorate("`⚠️`", "Error Message"),
|
|
||||||
value: `\`\`\`${truncatedErrorMessage}\`\`\``,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: decorate("`🧷`", "Build Link"),
|
|
||||||
value: `[Click here to access build link](${buildLink})`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
timestamp: date.toISOString(),
|
|
||||||
footer: {
|
|
||||||
text: "Dokploy Build Notification",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gotify) {
|
if (gotify) {
|
||||||
const decorate = (decoration: string, text: string) =>
|
const decorate = (decoration: string, text: string) =>
|
||||||
`${gotify.decoration ? decoration : ""} ${text}\n`;
|
`${gotify.decoration ? decoration : ""} ${text}\n`;
|
||||||
await sendGotifyNotification(
|
await sendGotifyNotification(
|
||||||
gotify,
|
gotify,
|
||||||
decorate("⚠️", "Build Failed"),
|
decorate("⚠️", "Build Failed"),
|
||||||
`${decorate("🛠️", `Project: ${projectName}`)}` +
|
`${decorate("🛠️", `Project: ${projectName}`)}` +
|
||||||
`${decorate("⚙️", `Application: ${applicationName}`)}` +
|
`${decorate("⚙️", `Application: ${applicationName}`)}` +
|
||||||
`${decorate("❔", `Type: ${applicationType}`)}` +
|
`${decorate("❔", `Type: ${applicationType}`)}` +
|
||||||
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
|
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
|
||||||
`${decorate("⚠️", `Error:\n${errorMessage}`)}` +
|
`${decorate("⚠️", `Error:\n${errorMessage}`)}` +
|
||||||
`${decorate("🔗", `Build details:\n${buildLink}`)}`,
|
`${decorate("🔗", `Build details:\n${buildLink}`)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ntfy) {
|
if (ntfy) {
|
||||||
await sendNtfyNotification(
|
await sendNtfyNotification(
|
||||||
ntfy,
|
ntfy,
|
||||||
"Build Failed",
|
"Build Failed",
|
||||||
"warning",
|
"warning",
|
||||||
`view, Build details, ${buildLink}, clear=true;`,
|
`view, Build details, ${buildLink}, clear=true;`,
|
||||||
`🛠️Project: ${projectName}\n` +
|
`🛠️Project: ${projectName}\n` +
|
||||||
`⚙️Application: ${applicationName}\n` +
|
`⚙️Application: ${applicationName}\n` +
|
||||||
`❔Type: ${applicationType}\n` +
|
`❔Type: ${applicationType}\n` +
|
||||||
`🕒Date: ${date.toLocaleString()}\n` +
|
`🕒Date: ${date.toLocaleString()}\n` +
|
||||||
`⚠️Error:\n${errorMessage}`,
|
`⚠️Error:\n${errorMessage}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (telegram) {
|
if (telegram) {
|
||||||
const inlineButton = [
|
const inlineButton = [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
text: "Deployment Logs",
|
text: "Deployment Logs",
|
||||||
url: buildLink,
|
url: buildLink,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
await sendTelegramNotification(
|
await sendTelegramNotification(
|
||||||
telegram,
|
telegram,
|
||||||
`<b>⚠️ Build Failed</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Type:</b> ${applicationType}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}\n\n<b>Error:</b>\n<pre>${errorMessage}</pre>`,
|
`<b>⚠️ Build Failed</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Type:</b> ${applicationType}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}\n\n<b>Error:</b>\n<pre>${errorMessage}</pre>`,
|
||||||
inlineButton,
|
inlineButton,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (slack) {
|
if (slack) {
|
||||||
const { channel } = slack;
|
const { channel } = slack;
|
||||||
await sendSlackNotification(slack, {
|
await sendSlackNotification(slack, {
|
||||||
channel: channel,
|
channel: channel,
|
||||||
attachments: [
|
attachments: [
|
||||||
{
|
{
|
||||||
color: "#FF0000",
|
color: "#FF0000",
|
||||||
pretext: ":warning: *Build Failed*",
|
pretext: ":warning: *Build Failed*",
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
title: "Project",
|
title: "Project",
|
||||||
value: projectName,
|
value: projectName,
|
||||||
short: true,
|
short: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Application",
|
title: "Application",
|
||||||
value: applicationName,
|
value: applicationName,
|
||||||
short: true,
|
short: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Type",
|
title: "Type",
|
||||||
value: applicationType,
|
value: applicationType,
|
||||||
short: true,
|
short: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Time",
|
title: "Time",
|
||||||
value: date.toLocaleString(),
|
value: date.toLocaleString(),
|
||||||
short: true,
|
short: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Error",
|
title: "Error",
|
||||||
value: `\`\`\`${errorMessage}\`\`\``,
|
value: `\`\`\`${errorMessage}\`\`\``,
|
||||||
short: false,
|
short: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
type: "button",
|
type: "button",
|
||||||
text: "View Build Details",
|
text: "View Build Details",
|
||||||
url: buildLink,
|
url: buildLink,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lark) {
|
if (lark) {
|
||||||
const limitCharacter = 800;
|
const limitCharacter = 800;
|
||||||
const truncatedErrorMessage = errorMessage.substring(0, limitCharacter);
|
const truncatedErrorMessage = errorMessage.substring(0, limitCharacter);
|
||||||
await sendLarkNotification(lark, {
|
await sendLarkNotification(lark, {
|
||||||
msg_type: "interactive",
|
msg_type: "interactive",
|
||||||
card: {
|
card: {
|
||||||
schema: "2.0",
|
schema: "2.0",
|
||||||
config: {
|
config: {
|
||||||
update_multi: true,
|
update_multi: true,
|
||||||
style: {
|
style: {
|
||||||
text_size: {
|
text_size: {
|
||||||
normal_v2: {
|
normal_v2: {
|
||||||
default: "normal",
|
default: "normal",
|
||||||
pc: "normal",
|
pc: "normal",
|
||||||
mobile: "heading",
|
mobile: "heading",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
header: {
|
||||||
header: {
|
title: {
|
||||||
title: {
|
tag: "plain_text",
|
||||||
tag: "plain_text",
|
content: "⚠️ Build Failed",
|
||||||
content: "⚠️ Build Failed",
|
|
||||||
},
|
|
||||||
subtitle: {
|
|
||||||
tag: "plain_text",
|
|
||||||
content: "",
|
|
||||||
},
|
|
||||||
template: "red",
|
|
||||||
padding: "12px 12px 12px 12px",
|
|
||||||
},
|
|
||||||
body: {
|
|
||||||
direction: "vertical",
|
|
||||||
padding: "12px 12px 12px 12px",
|
|
||||||
elements: [
|
|
||||||
{
|
|
||||||
tag: "column_set",
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
tag: "column",
|
|
||||||
width: "weighted",
|
|
||||||
elements: [
|
|
||||||
{
|
|
||||||
tag: "markdown",
|
|
||||||
content: `**Project:**\n${projectName}`,
|
|
||||||
text_align: "left",
|
|
||||||
text_size: "normal_v2",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tag: "markdown",
|
|
||||||
content: `**Type:**\n${applicationType}`,
|
|
||||||
text_align: "left",
|
|
||||||
text_size: "normal_v2",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tag: "markdown",
|
|
||||||
content: `**Error Message:**\n\`\`\`\n${truncatedErrorMessage}\n\`\`\``,
|
|
||||||
text_align: "left",
|
|
||||||
text_size: "normal_v2",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
vertical_align: "top",
|
|
||||||
weight: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tag: "column",
|
|
||||||
width: "weighted",
|
|
||||||
elements: [
|
|
||||||
{
|
|
||||||
tag: "markdown",
|
|
||||||
content: `**Application:**\n${applicationName}`,
|
|
||||||
text_align: "left",
|
|
||||||
text_size: "normal_v2",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tag: "markdown",
|
|
||||||
content: `**Date:**\n${format(date, "PP pp")}`,
|
|
||||||
text_align: "left",
|
|
||||||
text_size: "normal_v2",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
vertical_align: "top",
|
|
||||||
weight: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
subtitle: {
|
||||||
tag: "button",
|
tag: "plain_text",
|
||||||
text: {
|
content: "",
|
||||||
tag: "plain_text",
|
},
|
||||||
content: "View Build Details",
|
template: "red",
|
||||||
|
padding: "12px 12px 12px 12px",
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
direction: "vertical",
|
||||||
|
padding: "12px 12px 12px 12px",
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
tag: "column_set",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
tag: "column",
|
||||||
|
width: "weighted",
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Project:**\n${projectName}`,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Type:**\n${applicationType}`,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Error Message:**\n\`\`\`\n${truncatedErrorMessage}\n\`\`\``,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
vertical_align: "top",
|
||||||
|
weight: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "column",
|
||||||
|
width: "weighted",
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Application:**\n${applicationName}`,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Date:**\n${format(date, "PP pp")}`,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
vertical_align: "top",
|
||||||
|
weight: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
type: "danger",
|
{
|
||||||
width: "default",
|
tag: "button",
|
||||||
size: "medium",
|
text: {
|
||||||
behaviors: [
|
tag: "plain_text",
|
||||||
{
|
content: "View Build Details",
|
||||||
type: "open_url",
|
|
||||||
default_url: buildLink,
|
|
||||||
pc_url: "",
|
|
||||||
ios_url: "",
|
|
||||||
android_url: "",
|
|
||||||
},
|
},
|
||||||
],
|
type: "danger",
|
||||||
margin: "0px 0px 0px 0px",
|
width: "default",
|
||||||
},
|
size: "medium",
|
||||||
],
|
behaviors: [
|
||||||
|
{
|
||||||
|
type: "open_url",
|
||||||
|
default_url: buildLink,
|
||||||
|
pc_url: "",
|
||||||
|
ios_url: "",
|
||||||
|
android_url: "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
margin: "0px 0px 0px 0px",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
});
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import { and, eq } from "drizzle-orm";
|
|||||||
import {
|
import {
|
||||||
sendDiscordNotification,
|
sendDiscordNotification,
|
||||||
sendEmailNotification,
|
sendEmailNotification,
|
||||||
sendLarkNotification,
|
|
||||||
sendGotifyNotification,
|
sendGotifyNotification,
|
||||||
|
sendLarkNotification,
|
||||||
sendNtfyNotification,
|
sendNtfyNotification,
|
||||||
sendSlackNotification,
|
sendSlackNotification,
|
||||||
sendTelegramNotification,
|
sendTelegramNotification,
|
||||||
@@ -22,6 +22,7 @@ interface Props {
|
|||||||
buildLink: string;
|
buildLink: string;
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
domains: Domain[];
|
domains: Domain[];
|
||||||
|
environmentName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sendBuildSuccessNotifications = async ({
|
export const sendBuildSuccessNotifications = async ({
|
||||||
@@ -31,6 +32,7 @@ export const sendBuildSuccessNotifications = async ({
|
|||||||
buildLink,
|
buildLink,
|
||||||
organizationId,
|
organizationId,
|
||||||
domains,
|
domains,
|
||||||
|
environmentName,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
const unixDate = ~~(Number(date) / 1000);
|
const unixDate = ~~(Number(date) / 1000);
|
||||||
@@ -53,269 +55,295 @@ export const sendBuildSuccessNotifications = async ({
|
|||||||
for (const notification of notificationList) {
|
for (const notification of notificationList) {
|
||||||
const { email, discord, telegram, slack, gotify, ntfy, lark } =
|
const { email, discord, telegram, slack, gotify, ntfy, lark } =
|
||||||
notification;
|
notification;
|
||||||
|
try {
|
||||||
if (email) {
|
if (email) {
|
||||||
const template = await renderAsync(
|
const template = await renderAsync(
|
||||||
BuildSuccessEmail({
|
BuildSuccessEmail({
|
||||||
projectName,
|
projectName,
|
||||||
applicationName,
|
applicationName,
|
||||||
applicationType,
|
applicationType,
|
||||||
buildLink,
|
buildLink,
|
||||||
date: date.toLocaleString(),
|
date: date.toLocaleString(),
|
||||||
}),
|
environmentName,
|
||||||
).catch();
|
}),
|
||||||
await sendEmailNotification(email, "Build success for dokploy", template);
|
).catch();
|
||||||
}
|
await sendEmailNotification(
|
||||||
|
email,
|
||||||
if (discord) {
|
"Build success for dokploy",
|
||||||
const decorate = (decoration: string, text: string) =>
|
template,
|
||||||
`${discord.decoration ? decoration : ""} ${text}`.trim();
|
|
||||||
|
|
||||||
await sendDiscordNotification(discord, {
|
|
||||||
title: decorate(">", "`✅` Build Success"),
|
|
||||||
color: 0x57f287,
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
name: decorate("`🛠️`", "Project"),
|
|
||||||
value: projectName,
|
|
||||||
inline: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: decorate("`⚙️`", "Application"),
|
|
||||||
value: applicationName,
|
|
||||||
inline: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: decorate("`❔`", "Type"),
|
|
||||||
value: applicationType,
|
|
||||||
inline: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: decorate("`📅`", "Date"),
|
|
||||||
value: `<t:${unixDate}:D>`,
|
|
||||||
inline: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: decorate("`⌚`", "Time"),
|
|
||||||
value: `<t:${unixDate}:t>`,
|
|
||||||
inline: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: decorate("`❓`", "Type"),
|
|
||||||
value: "Successful",
|
|
||||||
inline: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: decorate("`🧷`", "Build Link"),
|
|
||||||
value: `[Click here to access build link](${buildLink})`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
timestamp: date.toISOString(),
|
|
||||||
footer: {
|
|
||||||
text: "Dokploy Build Notification",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gotify) {
|
|
||||||
const decorate = (decoration: string, text: string) =>
|
|
||||||
`${gotify.decoration ? decoration : ""} ${text}\n`;
|
|
||||||
await sendGotifyNotification(
|
|
||||||
gotify,
|
|
||||||
decorate("✅", "Build Success"),
|
|
||||||
`${decorate("🛠️", `Project: ${projectName}`)}` +
|
|
||||||
`${decorate("⚙️", `Application: ${applicationName}`)}` +
|
|
||||||
`${decorate("❔", `Type: ${applicationType}`)}` +
|
|
||||||
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
|
|
||||||
`${decorate("🔗", `Build details:\n${buildLink}`)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ntfy) {
|
|
||||||
await sendNtfyNotification(
|
|
||||||
ntfy,
|
|
||||||
"Build Success",
|
|
||||||
"white_check_mark",
|
|
||||||
`view, Build details, ${buildLink}, clear=true;`,
|
|
||||||
`🛠Project: ${projectName}\n` +
|
|
||||||
`⚙️Application: ${applicationName}\n` +
|
|
||||||
`❔Type: ${applicationType}\n` +
|
|
||||||
`🕒Date: ${date.toLocaleString()}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (telegram) {
|
|
||||||
const chunkArray = <T>(array: T[], chunkSize: number): T[][] =>
|
|
||||||
Array.from({ length: Math.ceil(array.length / chunkSize) }, (_, i) =>
|
|
||||||
array.slice(i * chunkSize, i * chunkSize + chunkSize),
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const inlineButton = [
|
if (discord) {
|
||||||
[
|
const decorate = (decoration: string, text: string) =>
|
||||||
{
|
`${discord.decoration ? decoration : ""} ${text}`.trim();
|
||||||
text: "Deployment Logs",
|
|
||||||
url: buildLink,
|
await sendDiscordNotification(discord, {
|
||||||
|
title: decorate(">", "`✅` Build Successes"),
|
||||||
|
color: 0x57f287,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: decorate("`🛠️`", "Project"),
|
||||||
|
value: projectName,
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: decorate("`⚙️`", "Application"),
|
||||||
|
value: applicationName,
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: decorate("`🌍`", "Environment"),
|
||||||
|
value: environmentName,
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: decorate("`❔`", "Type"),
|
||||||
|
value: applicationType,
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: decorate("`📅`", "Date"),
|
||||||
|
value: `<t:${unixDate}:D>`,
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: decorate("`⌚`", "Time"),
|
||||||
|
value: `<t:${unixDate}:t>`,
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: decorate("`❓`", "Type"),
|
||||||
|
value: "Successful",
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: decorate("`🧷`", "Build Link"),
|
||||||
|
value: `[Click here to access build link](${buildLink})`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
timestamp: date.toISOString(),
|
||||||
|
footer: {
|
||||||
|
text: "Dokploy Build Notification",
|
||||||
},
|
},
|
||||||
],
|
});
|
||||||
...chunkArray(domains, 2).map((chunk) =>
|
}
|
||||||
chunk.map((data) => ({
|
|
||||||
text: data.host,
|
|
||||||
url: `${data.https ? "https" : "http"}://${data.host}`,
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
await sendTelegramNotification(
|
if (gotify) {
|
||||||
telegram,
|
const decorate = (decoration: string, text: string) =>
|
||||||
`<b>✅ Build Success</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Type:</b> ${applicationType}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`,
|
`${gotify.decoration ? decoration : ""} ${text}\n`;
|
||||||
inlineButton,
|
await sendGotifyNotification(
|
||||||
);
|
gotify,
|
||||||
}
|
decorate("✅", "Build Success"),
|
||||||
|
`${decorate("🛠️", `Project: ${projectName}`)}` +
|
||||||
|
`${decorate("⚙️", `Application: ${applicationName}`)}` +
|
||||||
|
`${decorate("🌍", `Environment: ${environmentName}`)}` +
|
||||||
|
`${decorate("❔", `Type: ${applicationType}`)}` +
|
||||||
|
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
|
||||||
|
`${decorate("🔗", `Build details:\n${buildLink}`)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (slack) {
|
if (ntfy) {
|
||||||
const { channel } = slack;
|
await sendNtfyNotification(
|
||||||
await sendSlackNotification(slack, {
|
ntfy,
|
||||||
channel: channel,
|
"Build Success",
|
||||||
attachments: [
|
"white_check_mark",
|
||||||
{
|
`view, Build details, ${buildLink}, clear=true;`,
|
||||||
color: "#00FF00",
|
`🛠Project: ${projectName}\n` +
|
||||||
pretext: ":white_check_mark: *Build Success*",
|
`⚙️Application: ${applicationName}\n` +
|
||||||
fields: [
|
`🌍Environment: ${environmentName}\n` +
|
||||||
{
|
`❔Type: ${applicationType}\n` +
|
||||||
title: "Project",
|
`🕒Date: ${date.toLocaleString()}`,
|
||||||
value: projectName,
|
);
|
||||||
short: true,
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Application",
|
|
||||||
value: applicationName,
|
|
||||||
short: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Type",
|
|
||||||
value: applicationType,
|
|
||||||
short: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Time",
|
|
||||||
value: date.toLocaleString(),
|
|
||||||
short: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
type: "button",
|
|
||||||
text: "View Build Details",
|
|
||||||
url: buildLink,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lark) {
|
if (telegram) {
|
||||||
await sendLarkNotification(lark, {
|
const chunkArray = <T>(array: T[], chunkSize: number): T[][] =>
|
||||||
msg_type: "interactive",
|
Array.from({ length: Math.ceil(array.length / chunkSize) }, (_, i) =>
|
||||||
card: {
|
array.slice(i * chunkSize, i * chunkSize + chunkSize),
|
||||||
schema: "2.0",
|
);
|
||||||
config: {
|
|
||||||
update_multi: true,
|
const inlineButton = [
|
||||||
style: {
|
[
|
||||||
text_size: {
|
{
|
||||||
normal_v2: {
|
text: "Deployment Logs",
|
||||||
default: "normal",
|
url: buildLink,
|
||||||
pc: "normal",
|
},
|
||||||
mobile: "heading",
|
],
|
||||||
|
...chunkArray(domains, 2).map((chunk) =>
|
||||||
|
chunk.map((data) => ({
|
||||||
|
text: data.host,
|
||||||
|
url: `${data.https ? "https" : "http"}://${data.host}`,
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
await sendTelegramNotification(
|
||||||
|
telegram,
|
||||||
|
`<b>✅ Build Success</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Environment:</b> ${environmentName}\n<b>Type:</b> ${applicationType}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`,
|
||||||
|
inlineButton,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (slack) {
|
||||||
|
const { channel } = slack;
|
||||||
|
await sendSlackNotification(slack, {
|
||||||
|
channel: channel,
|
||||||
|
attachments: [
|
||||||
|
{
|
||||||
|
color: "#00FF00",
|
||||||
|
pretext: ":white_check_mark: *Build Success*",
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
title: "Project",
|
||||||
|
value: projectName,
|
||||||
|
short: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Application",
|
||||||
|
value: applicationName,
|
||||||
|
short: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Environment",
|
||||||
|
value: environmentName,
|
||||||
|
short: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Type",
|
||||||
|
value: applicationType,
|
||||||
|
short: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Time",
|
||||||
|
value: date.toLocaleString(),
|
||||||
|
short: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
type: "button",
|
||||||
|
text: "View Build Details",
|
||||||
|
url: buildLink,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lark) {
|
||||||
|
await sendLarkNotification(lark, {
|
||||||
|
msg_type: "interactive",
|
||||||
|
card: {
|
||||||
|
schema: "2.0",
|
||||||
|
config: {
|
||||||
|
update_multi: true,
|
||||||
|
style: {
|
||||||
|
text_size: {
|
||||||
|
normal_v2: {
|
||||||
|
default: "normal",
|
||||||
|
pc: "normal",
|
||||||
|
mobile: "heading",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
header: {
|
||||||
header: {
|
title: {
|
||||||
title: {
|
tag: "plain_text",
|
||||||
tag: "plain_text",
|
content: "✅ Build Success",
|
||||||
content: "✅ Build Success",
|
|
||||||
},
|
|
||||||
subtitle: {
|
|
||||||
tag: "plain_text",
|
|
||||||
content: "",
|
|
||||||
},
|
|
||||||
template: "green",
|
|
||||||
padding: "12px 12px 12px 12px",
|
|
||||||
},
|
|
||||||
body: {
|
|
||||||
direction: "vertical",
|
|
||||||
padding: "12px 12px 12px 12px",
|
|
||||||
elements: [
|
|
||||||
{
|
|
||||||
tag: "column_set",
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
tag: "column",
|
|
||||||
width: "weighted",
|
|
||||||
elements: [
|
|
||||||
{
|
|
||||||
tag: "markdown",
|
|
||||||
content: `**Project:**\n${projectName}`,
|
|
||||||
text_align: "left",
|
|
||||||
text_size: "normal_v2",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tag: "markdown",
|
|
||||||
content: `**Type:**\n${applicationType}`,
|
|
||||||
text_align: "left",
|
|
||||||
text_size: "normal_v2",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
vertical_align: "top",
|
|
||||||
weight: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tag: "column",
|
|
||||||
width: "weighted",
|
|
||||||
elements: [
|
|
||||||
{
|
|
||||||
tag: "markdown",
|
|
||||||
content: `**Application:**\n${applicationName}`,
|
|
||||||
text_align: "left",
|
|
||||||
text_size: "normal_v2",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tag: "markdown",
|
|
||||||
content: `**Date:**\n${format(date, "PP pp")}`,
|
|
||||||
text_align: "left",
|
|
||||||
text_size: "normal_v2",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
vertical_align: "top",
|
|
||||||
weight: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
subtitle: {
|
||||||
tag: "button",
|
tag: "plain_text",
|
||||||
text: {
|
content: "",
|
||||||
tag: "plain_text",
|
},
|
||||||
content: "View Build Details",
|
template: "green",
|
||||||
|
padding: "12px 12px 12px 12px",
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
direction: "vertical",
|
||||||
|
padding: "12px 12px 12px 12px",
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
tag: "column_set",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
tag: "column",
|
||||||
|
width: "weighted",
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Project:**\n${projectName}`,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Environment:**\n${environmentName}`,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Type:**\n${applicationType}`,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
vertical_align: "top",
|
||||||
|
weight: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "column",
|
||||||
|
width: "weighted",
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Application:**\n${applicationName}`,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Date:**\n${format(date, "PP pp")}`,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
vertical_align: "top",
|
||||||
|
weight: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
type: "primary",
|
{
|
||||||
width: "default",
|
tag: "button",
|
||||||
size: "medium",
|
text: {
|
||||||
behaviors: [
|
tag: "plain_text",
|
||||||
{
|
content: "View Build Details",
|
||||||
type: "open_url",
|
|
||||||
default_url: buildLink,
|
|
||||||
pc_url: "",
|
|
||||||
ios_url: "",
|
|
||||||
android_url: "",
|
|
||||||
},
|
},
|
||||||
],
|
type: "primary",
|
||||||
margin: "0px 0px 0px 0px",
|
width: "default",
|
||||||
},
|
size: "medium",
|
||||||
],
|
behaviors: [
|
||||||
|
{
|
||||||
|
type: "open_url",
|
||||||
|
default_url: buildLink,
|
||||||
|
pc_url: "",
|
||||||
|
ios_url: "",
|
||||||
|
android_url: "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
margin: "0px 0px 0px 0px",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
});
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import { and, eq } from "drizzle-orm";
|
|||||||
import {
|
import {
|
||||||
sendDiscordNotification,
|
sendDiscordNotification,
|
||||||
sendEmailNotification,
|
sendEmailNotification,
|
||||||
sendLarkNotification,
|
|
||||||
sendGotifyNotification,
|
sendGotifyNotification,
|
||||||
|
sendLarkNotification,
|
||||||
sendNtfyNotification,
|
sendNtfyNotification,
|
||||||
sendSlackNotification,
|
sendSlackNotification,
|
||||||
sendTelegramNotification,
|
sendTelegramNotification,
|
||||||
@@ -52,309 +52,312 @@ export const sendDatabaseBackupNotifications = async ({
|
|||||||
for (const notification of notificationList) {
|
for (const notification of notificationList) {
|
||||||
const { email, discord, telegram, slack, gotify, ntfy, lark } =
|
const { email, discord, telegram, slack, gotify, ntfy, lark } =
|
||||||
notification;
|
notification;
|
||||||
|
try {
|
||||||
|
if (email) {
|
||||||
|
const template = await renderAsync(
|
||||||
|
DatabaseBackupEmail({
|
||||||
|
projectName,
|
||||||
|
applicationName,
|
||||||
|
databaseType,
|
||||||
|
type,
|
||||||
|
errorMessage,
|
||||||
|
date: date.toLocaleString(),
|
||||||
|
}),
|
||||||
|
).catch();
|
||||||
|
await sendEmailNotification(
|
||||||
|
email,
|
||||||
|
"Database backup for dokploy",
|
||||||
|
template,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (email) {
|
if (discord) {
|
||||||
const template = await renderAsync(
|
const decorate = (decoration: string, text: string) =>
|
||||||
DatabaseBackupEmail({
|
`${discord.decoration ? decoration : ""} ${text}`.trim();
|
||||||
projectName,
|
|
||||||
applicationName,
|
|
||||||
databaseType,
|
|
||||||
type,
|
|
||||||
errorMessage,
|
|
||||||
date: date.toLocaleString(),
|
|
||||||
}),
|
|
||||||
).catch();
|
|
||||||
await sendEmailNotification(
|
|
||||||
email,
|
|
||||||
"Database backup for dokploy",
|
|
||||||
template,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (discord) {
|
await sendDiscordNotification(discord, {
|
||||||
const decorate = (decoration: string, text: string) =>
|
title:
|
||||||
`${discord.decoration ? decoration : ""} ${text}`.trim();
|
type === "success"
|
||||||
|
? decorate(">", "`✅` Database Backup Successful")
|
||||||
|
: decorate(">", "`❌` Database Backup Failed"),
|
||||||
|
color: type === "success" ? 0x57f287 : 0xed4245,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: decorate("`🛠️`", "Project"),
|
||||||
|
value: projectName,
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: decorate("`⚙️`", "Application"),
|
||||||
|
value: applicationName,
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: decorate("`❔`", "Database"),
|
||||||
|
value: databaseType,
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: decorate("`📂`", "Database Name"),
|
||||||
|
value: databaseName,
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: decorate("`📅`", "Date"),
|
||||||
|
value: `<t:${unixDate}:D>`,
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: decorate("`⌚`", "Time"),
|
||||||
|
value: `<t:${unixDate}:t>`,
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: decorate("`❓`", "Type"),
|
||||||
|
value: type
|
||||||
|
.replace("error", "Failed")
|
||||||
|
.replace("success", "Successful"),
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
...(type === "error" && errorMessage
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
name: decorate("`⚠️`", "Error Message"),
|
||||||
|
value: `\`\`\`${errorMessage}\`\`\``,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
],
|
||||||
|
timestamp: date.toISOString(),
|
||||||
|
footer: {
|
||||||
|
text: "Dokploy Database Backup Notification",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await sendDiscordNotification(discord, {
|
if (gotify) {
|
||||||
title:
|
const decorate = (decoration: string, text: string) =>
|
||||||
type === "success"
|
`${gotify.decoration ? decoration : ""} ${text}\n`;
|
||||||
? decorate(">", "`✅` Database Backup Successful")
|
|
||||||
: decorate(">", "`❌` Database Backup Failed"),
|
|
||||||
color: type === "success" ? 0x57f287 : 0xed4245,
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
name: decorate("`🛠️`", "Project"),
|
|
||||||
value: projectName,
|
|
||||||
inline: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: decorate("`⚙️`", "Application"),
|
|
||||||
value: applicationName,
|
|
||||||
inline: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: decorate("`❔`", "Database"),
|
|
||||||
value: databaseType,
|
|
||||||
inline: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: decorate("`📂`", "Database Name"),
|
|
||||||
value: databaseName,
|
|
||||||
inline: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: decorate("`📅`", "Date"),
|
|
||||||
value: `<t:${unixDate}:D>`,
|
|
||||||
inline: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: decorate("`⌚`", "Time"),
|
|
||||||
value: `<t:${unixDate}:t>`,
|
|
||||||
inline: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: decorate("`❓`", "Type"),
|
|
||||||
value: type
|
|
||||||
.replace("error", "Failed")
|
|
||||||
.replace("success", "Successful"),
|
|
||||||
inline: true,
|
|
||||||
},
|
|
||||||
...(type === "error" && errorMessage
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
name: decorate("`⚠️`", "Error Message"),
|
|
||||||
value: `\`\`\`${errorMessage}\`\`\``,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
],
|
|
||||||
timestamp: date.toISOString(),
|
|
||||||
footer: {
|
|
||||||
text: "Dokploy Database Backup Notification",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gotify) {
|
await sendGotifyNotification(
|
||||||
const decorate = (decoration: string, text: string) =>
|
gotify,
|
||||||
`${gotify.decoration ? decoration : ""} ${text}\n`;
|
decorate(
|
||||||
|
type === "success" ? "✅" : "❌",
|
||||||
|
`Database Backup ${type === "success" ? "Successful" : "Failed"}`,
|
||||||
|
),
|
||||||
|
`${decorate("🛠️", `Project: ${projectName}`)}` +
|
||||||
|
`${decorate("⚙️", `Application: ${applicationName}`)}` +
|
||||||
|
`${decorate("❔", `Type: ${databaseType}`)}` +
|
||||||
|
`${decorate("📂", `Database Name: ${databaseName}`)}` +
|
||||||
|
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
|
||||||
|
`${type === "error" && errorMessage ? decorate("❌", `Error:\n${errorMessage}`) : ""}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await sendGotifyNotification(
|
if (ntfy) {
|
||||||
gotify,
|
await sendNtfyNotification(
|
||||||
decorate(
|
ntfy,
|
||||||
type === "success" ? "✅" : "❌",
|
|
||||||
`Database Backup ${type === "success" ? "Successful" : "Failed"}`,
|
`Database Backup ${type === "success" ? "Successful" : "Failed"}`,
|
||||||
),
|
`${type === "success" ? "white_check_mark" : "x"}`,
|
||||||
`${decorate("🛠️", `Project: ${projectName}`)}` +
|
"",
|
||||||
`${decorate("⚙️", `Application: ${applicationName}`)}` +
|
`🛠Project: ${projectName}\n` +
|
||||||
`${decorate("❔", `Type: ${databaseType}`)}` +
|
`⚙️Application: ${applicationName}\n` +
|
||||||
`${decorate("📂", `Database Name: ${databaseName}`)}` +
|
`❔Type: ${databaseType}\n` +
|
||||||
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
|
`📂Database Name: ${databaseName}` +
|
||||||
`${type === "error" && errorMessage ? decorate("❌", `Error:\n${errorMessage}`) : ""}`,
|
`🕒Date: ${date.toLocaleString()}\n` +
|
||||||
);
|
`${type === "error" && errorMessage ? `❌Error:\n${errorMessage}` : ""}`,
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (ntfy) {
|
if (telegram) {
|
||||||
await sendNtfyNotification(
|
const isError = type === "error" && errorMessage;
|
||||||
ntfy,
|
|
||||||
`Database Backup ${type === "success" ? "Successful" : "Failed"}`,
|
|
||||||
`${type === "success" ? "white_check_mark" : "x"}`,
|
|
||||||
"",
|
|
||||||
`🛠Project: ${projectName}\n` +
|
|
||||||
`⚙️Application: ${applicationName}\n` +
|
|
||||||
`❔Type: ${databaseType}\n` +
|
|
||||||
`📂Database Name: ${databaseName}` +
|
|
||||||
`🕒Date: ${date.toLocaleString()}\n` +
|
|
||||||
`${type === "error" && errorMessage ? `❌Error:\n${errorMessage}` : ""}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (telegram) {
|
const statusEmoji = type === "success" ? "✅" : "❌";
|
||||||
const isError = type === "error" && errorMessage;
|
const typeStatus = type === "success" ? "Successful" : "Failed";
|
||||||
|
const errorMsg = isError
|
||||||
|
? `\n\n<b>Error:</b>\n<pre>${errorMessage}</pre>`
|
||||||
|
: "";
|
||||||
|
|
||||||
const statusEmoji = type === "success" ? "✅" : "❌";
|
const messageText = `<b>${statusEmoji} Database Backup ${typeStatus}</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Type:</b> ${databaseType}\n<b>Database Name:</b> ${databaseName}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}${isError ? errorMsg : ""}`;
|
||||||
const typeStatus = type === "success" ? "Successful" : "Failed";
|
|
||||||
const errorMsg = isError
|
|
||||||
? `\n\n<b>Error:</b>\n<pre>${errorMessage}</pre>`
|
|
||||||
: "";
|
|
||||||
|
|
||||||
const messageText = `<b>${statusEmoji} Database Backup ${typeStatus}</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Type:</b> ${databaseType}\n<b>Database Name:</b> ${databaseName}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}${isError ? errorMsg : ""}`;
|
await sendTelegramNotification(telegram, messageText);
|
||||||
|
}
|
||||||
|
|
||||||
await sendTelegramNotification(telegram, messageText);
|
if (slack) {
|
||||||
}
|
const { channel } = slack;
|
||||||
|
await sendSlackNotification(slack, {
|
||||||
|
channel: channel,
|
||||||
|
attachments: [
|
||||||
|
{
|
||||||
|
color: type === "success" ? "#00FF00" : "#FF0000",
|
||||||
|
pretext:
|
||||||
|
type === "success"
|
||||||
|
? ":white_check_mark: *Database Backup Successful*"
|
||||||
|
: ":x: *Database Backup Failed*",
|
||||||
|
fields: [
|
||||||
|
...(type === "error" && errorMessage
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
title: "Error Message",
|
||||||
|
value: errorMessage,
|
||||||
|
short: false,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
{
|
||||||
|
title: "Project",
|
||||||
|
value: projectName,
|
||||||
|
short: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Application",
|
||||||
|
value: applicationName,
|
||||||
|
short: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Type",
|
||||||
|
value: databaseType,
|
||||||
|
short: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Database Name",
|
||||||
|
value: databaseName,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Time",
|
||||||
|
value: date.toLocaleString(),
|
||||||
|
short: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Type",
|
||||||
|
value: type,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Status",
|
||||||
|
value: type === "success" ? "Successful" : "Failed",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (slack) {
|
if (lark) {
|
||||||
const { channel } = slack;
|
const limitCharacter = 800;
|
||||||
await sendSlackNotification(slack, {
|
const truncatedErrorMessage =
|
||||||
channel: channel,
|
errorMessage && errorMessage.length > limitCharacter
|
||||||
attachments: [
|
? errorMessage.substring(0, limitCharacter)
|
||||||
{
|
: errorMessage;
|
||||||
color: type === "success" ? "#00FF00" : "#FF0000",
|
|
||||||
pretext:
|
|
||||||
type === "success"
|
|
||||||
? ":white_check_mark: *Database Backup Successful*"
|
|
||||||
: ":x: *Database Backup Failed*",
|
|
||||||
fields: [
|
|
||||||
...(type === "error" && errorMessage
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
title: "Error Message",
|
|
||||||
value: errorMessage,
|
|
||||||
short: false,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
{
|
|
||||||
title: "Project",
|
|
||||||
value: projectName,
|
|
||||||
short: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Application",
|
|
||||||
value: applicationName,
|
|
||||||
short: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Type",
|
|
||||||
value: databaseType,
|
|
||||||
short: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Database Name",
|
|
||||||
value: databaseName,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Time",
|
|
||||||
value: date.toLocaleString(),
|
|
||||||
short: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Type",
|
|
||||||
value: type,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Status",
|
|
||||||
value: type === "success" ? "Successful" : "Failed",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lark) {
|
await sendLarkNotification(lark, {
|
||||||
const limitCharacter = 800;
|
msg_type: "interactive",
|
||||||
const truncatedErrorMessage =
|
card: {
|
||||||
errorMessage && errorMessage.length > limitCharacter
|
schema: "2.0",
|
||||||
? errorMessage.substring(0, limitCharacter)
|
config: {
|
||||||
: errorMessage;
|
update_multi: true,
|
||||||
|
style: {
|
||||||
await sendLarkNotification(lark, {
|
text_size: {
|
||||||
msg_type: "interactive",
|
normal_v2: {
|
||||||
card: {
|
default: "normal",
|
||||||
schema: "2.0",
|
pc: "normal",
|
||||||
config: {
|
mobile: "heading",
|
||||||
update_multi: true,
|
},
|
||||||
style: {
|
|
||||||
text_size: {
|
|
||||||
normal_v2: {
|
|
||||||
default: "normal",
|
|
||||||
pc: "normal",
|
|
||||||
mobile: "heading",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
header: {
|
||||||
header: {
|
title: {
|
||||||
title: {
|
tag: "plain_text",
|
||||||
tag: "plain_text",
|
content:
|
||||||
content:
|
type === "success"
|
||||||
type === "success"
|
? "✅ Database Backup Successful"
|
||||||
? "✅ Database Backup Successful"
|
: "❌ Database Backup Failed",
|
||||||
: "❌ Database Backup Failed",
|
|
||||||
},
|
|
||||||
subtitle: {
|
|
||||||
tag: "plain_text",
|
|
||||||
content: "",
|
|
||||||
},
|
|
||||||
template: type === "success" ? "green" : "red",
|
|
||||||
padding: "12px 12px 12px 12px",
|
|
||||||
},
|
|
||||||
body: {
|
|
||||||
direction: "vertical",
|
|
||||||
padding: "12px 12px 12px 12px",
|
|
||||||
elements: [
|
|
||||||
{
|
|
||||||
tag: "column_set",
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
tag: "column",
|
|
||||||
width: "weighted",
|
|
||||||
elements: [
|
|
||||||
{
|
|
||||||
tag: "markdown",
|
|
||||||
content: `**Project:**\n${projectName}`,
|
|
||||||
text_align: "left",
|
|
||||||
text_size: "normal_v2",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tag: "markdown",
|
|
||||||
content: `**Database Type:**\n${databaseType}`,
|
|
||||||
text_align: "left",
|
|
||||||
text_size: "normal_v2",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tag: "markdown",
|
|
||||||
content: `**Status:**\n${type === "success" ? "Successful" : "Failed"}`,
|
|
||||||
text_align: "left",
|
|
||||||
text_size: "normal_v2",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
vertical_align: "top",
|
|
||||||
weight: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tag: "column",
|
|
||||||
width: "weighted",
|
|
||||||
elements: [
|
|
||||||
{
|
|
||||||
tag: "markdown",
|
|
||||||
content: `**Application:**\n${applicationName}`,
|
|
||||||
text_align: "left",
|
|
||||||
text_size: "normal_v2",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tag: "markdown",
|
|
||||||
content: `**Database Name:**\n${databaseName}`,
|
|
||||||
text_align: "left",
|
|
||||||
text_size: "normal_v2",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tag: "markdown",
|
|
||||||
content: `**Date:**\n${format(date, "PP pp")}`,
|
|
||||||
text_align: "left",
|
|
||||||
text_size: "normal_v2",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
vertical_align: "top",
|
|
||||||
weight: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
...(type === "error" && truncatedErrorMessage
|
subtitle: {
|
||||||
? [
|
tag: "plain_text",
|
||||||
|
content: "",
|
||||||
|
},
|
||||||
|
template: type === "success" ? "green" : "red",
|
||||||
|
padding: "12px 12px 12px 12px",
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
direction: "vertical",
|
||||||
|
padding: "12px 12px 12px 12px",
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
tag: "column_set",
|
||||||
|
columns: [
|
||||||
{
|
{
|
||||||
tag: "markdown",
|
tag: "column",
|
||||||
content: `**Error Message:**\n\`\`\`\n${truncatedErrorMessage}\n\`\`\``,
|
width: "weighted",
|
||||||
text_align: "left",
|
elements: [
|
||||||
text_size: "normal_v2",
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Project:**\n${projectName}`,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Database Type:**\n${databaseType}`,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Status:**\n${type === "success" ? "Successful" : "Failed"}`,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
vertical_align: "top",
|
||||||
|
weight: 1,
|
||||||
},
|
},
|
||||||
]
|
{
|
||||||
: []),
|
tag: "column",
|
||||||
],
|
width: "weighted",
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Application:**\n${applicationName}`,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Database Name:**\n${databaseName}`,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Date:**\n${format(date, "PP pp")}`,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
vertical_align: "top",
|
||||||
|
weight: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
...(type === "error" && truncatedErrorMessage
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Error Message:**\n\`\`\`\n${truncatedErrorMessage}\n\`\`\``,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
});
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import { and, eq } from "drizzle-orm";
|
|||||||
import {
|
import {
|
||||||
sendDiscordNotification,
|
sendDiscordNotification,
|
||||||
sendEmailNotification,
|
sendEmailNotification,
|
||||||
sendLarkNotification,
|
|
||||||
sendGotifyNotification,
|
sendGotifyNotification,
|
||||||
|
sendLarkNotification,
|
||||||
sendNtfyNotification,
|
sendNtfyNotification,
|
||||||
sendSlackNotification,
|
sendSlackNotification,
|
||||||
sendTelegramNotification,
|
sendTelegramNotification,
|
||||||
@@ -39,182 +39,185 @@ export const sendDockerCleanupNotifications = async (
|
|||||||
for (const notification of notificationList) {
|
for (const notification of notificationList) {
|
||||||
const { email, discord, telegram, slack, gotify, ntfy, lark } =
|
const { email, discord, telegram, slack, gotify, ntfy, lark } =
|
||||||
notification;
|
notification;
|
||||||
|
try {
|
||||||
|
if (email) {
|
||||||
|
const template = await renderAsync(
|
||||||
|
DockerCleanupEmail({ message, date: date.toLocaleString() }),
|
||||||
|
).catch();
|
||||||
|
|
||||||
if (email) {
|
await sendEmailNotification(
|
||||||
const template = await renderAsync(
|
email,
|
||||||
DockerCleanupEmail({ message, date: date.toLocaleString() }),
|
"Docker cleanup for dokploy",
|
||||||
).catch();
|
template,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await sendEmailNotification(
|
if (discord) {
|
||||||
email,
|
const decorate = (decoration: string, text: string) =>
|
||||||
"Docker cleanup for dokploy",
|
`${discord.decoration ? decoration : ""} ${text}`.trim();
|
||||||
template,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (discord) {
|
await sendDiscordNotification(discord, {
|
||||||
const decorate = (decoration: string, text: string) =>
|
title: decorate(">", "`✅` Docker Cleanup"),
|
||||||
`${discord.decoration ? decoration : ""} ${text}`.trim();
|
color: 0x57f287,
|
||||||
|
fields: [
|
||||||
await sendDiscordNotification(discord, {
|
{
|
||||||
title: decorate(">", "`✅` Docker Cleanup"),
|
name: decorate("`📅`", "Date"),
|
||||||
color: 0x57f287,
|
value: `<t:${unixDate}:D>`,
|
||||||
fields: [
|
inline: true,
|
||||||
{
|
},
|
||||||
name: decorate("`📅`", "Date"),
|
{
|
||||||
value: `<t:${unixDate}:D>`,
|
name: decorate("`⌚`", "Time"),
|
||||||
inline: true,
|
value: `<t:${unixDate}:t>`,
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: decorate("`❓`", "Type"),
|
||||||
|
value: "Successful",
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: decorate("`📜`", "Message"),
|
||||||
|
value: `\`\`\`${message}\`\`\``,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
timestamp: date.toISOString(),
|
||||||
|
footer: {
|
||||||
|
text: "Dokploy Docker Cleanup Notification",
|
||||||
},
|
},
|
||||||
{
|
});
|
||||||
name: decorate("`⌚`", "Time"),
|
}
|
||||||
value: `<t:${unixDate}:t>`,
|
|
||||||
inline: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: decorate("`❓`", "Type"),
|
|
||||||
value: "Successful",
|
|
||||||
inline: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: decorate("`📜`", "Message"),
|
|
||||||
value: `\`\`\`${message}\`\`\``,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
timestamp: date.toISOString(),
|
|
||||||
footer: {
|
|
||||||
text: "Dokploy Docker Cleanup Notification",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gotify) {
|
if (gotify) {
|
||||||
const decorate = (decoration: string, text: string) =>
|
const decorate = (decoration: string, text: string) =>
|
||||||
`${gotify.decoration ? decoration : ""} ${text}\n`;
|
`${gotify.decoration ? decoration : ""} ${text}\n`;
|
||||||
await sendGotifyNotification(
|
await sendGotifyNotification(
|
||||||
gotify,
|
gotify,
|
||||||
decorate("✅", "Docker Cleanup"),
|
decorate("✅", "Docker Cleanup"),
|
||||||
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
|
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
|
||||||
`${decorate("📜", `Message:\n${message}`)}`,
|
`${decorate("📜", `Message:\n${message}`)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ntfy) {
|
if (ntfy) {
|
||||||
await sendNtfyNotification(
|
await sendNtfyNotification(
|
||||||
ntfy,
|
ntfy,
|
||||||
"Docker Cleanup",
|
"Docker Cleanup",
|
||||||
"white_check_mark",
|
"white_check_mark",
|
||||||
"",
|
"",
|
||||||
`🕒Date: ${date.toLocaleString()}\n` + `📜Message:\n${message}`,
|
`🕒Date: ${date.toLocaleString()}\n` + `📜Message:\n${message}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (telegram) {
|
if (telegram) {
|
||||||
await sendTelegramNotification(
|
await sendTelegramNotification(
|
||||||
telegram,
|
telegram,
|
||||||
`<b>✅ Docker Cleanup</b>\n\n<b>Message:</b> ${message}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`,
|
`<b>✅ Docker Cleanup</b>\n\n<b>Message:</b> ${message}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (slack) {
|
if (slack) {
|
||||||
const { channel } = slack;
|
const { channel } = slack;
|
||||||
await sendSlackNotification(slack, {
|
await sendSlackNotification(slack, {
|
||||||
channel: channel,
|
channel: channel,
|
||||||
attachments: [
|
attachments: [
|
||||||
{
|
{
|
||||||
color: "#00FF00",
|
color: "#00FF00",
|
||||||
pretext: ":white_check_mark: *Docker Cleanup*",
|
pretext: ":white_check_mark: *Docker Cleanup*",
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
title: "Message",
|
title: "Message",
|
||||||
value: message,
|
value: message,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Time",
|
title: "Time",
|
||||||
value: date.toLocaleString(),
|
value: date.toLocaleString(),
|
||||||
short: true,
|
short: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lark) {
|
if (lark) {
|
||||||
await sendLarkNotification(lark, {
|
await sendLarkNotification(lark, {
|
||||||
msg_type: "interactive",
|
msg_type: "interactive",
|
||||||
card: {
|
card: {
|
||||||
schema: "2.0",
|
schema: "2.0",
|
||||||
config: {
|
config: {
|
||||||
update_multi: true,
|
update_multi: true,
|
||||||
style: {
|
style: {
|
||||||
text_size: {
|
text_size: {
|
||||||
normal_v2: {
|
normal_v2: {
|
||||||
default: "normal",
|
default: "normal",
|
||||||
pc: "normal",
|
pc: "normal",
|
||||||
mobile: "heading",
|
mobile: "heading",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
header: {
|
||||||
header: {
|
title: {
|
||||||
title: {
|
tag: "plain_text",
|
||||||
tag: "plain_text",
|
content: "✅ Docker Cleanup",
|
||||||
content: "✅ Docker Cleanup",
|
|
||||||
},
|
|
||||||
subtitle: {
|
|
||||||
tag: "plain_text",
|
|
||||||
content: "",
|
|
||||||
},
|
|
||||||
template: "green",
|
|
||||||
padding: "12px 12px 12px 12px",
|
|
||||||
},
|
|
||||||
body: {
|
|
||||||
direction: "vertical",
|
|
||||||
padding: "12px 12px 12px 12px",
|
|
||||||
elements: [
|
|
||||||
{
|
|
||||||
tag: "column_set",
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
tag: "column",
|
|
||||||
width: "weighted",
|
|
||||||
elements: [
|
|
||||||
{
|
|
||||||
tag: "markdown",
|
|
||||||
content: `**Status:**\nSuccessful`,
|
|
||||||
text_align: "left",
|
|
||||||
text_size: "normal_v2",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tag: "markdown",
|
|
||||||
content: `**Cleanup Details:**\n${message}`,
|
|
||||||
text_align: "left",
|
|
||||||
text_size: "normal_v2",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
vertical_align: "top",
|
|
||||||
weight: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tag: "column",
|
|
||||||
width: "weighted",
|
|
||||||
elements: [
|
|
||||||
{
|
|
||||||
tag: "markdown",
|
|
||||||
content: `**Date:**\n${format(date, "PP pp")}`,
|
|
||||||
text_align: "left",
|
|
||||||
text_size: "normal_v2",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
vertical_align: "top",
|
|
||||||
weight: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
subtitle: {
|
||||||
|
tag: "plain_text",
|
||||||
|
content: "",
|
||||||
|
},
|
||||||
|
template: "green",
|
||||||
|
padding: "12px 12px 12px 12px",
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
direction: "vertical",
|
||||||
|
padding: "12px 12px 12px 12px",
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
tag: "column_set",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
tag: "column",
|
||||||
|
width: "weighted",
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: "**Status:**\nSuccessful",
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Cleanup Details:**\n${message}`,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
vertical_align: "top",
|
||||||
|
weight: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "column",
|
||||||
|
width: "weighted",
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Date:**\n${format(date, "PP pp")}`,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
vertical_align: "top",
|
||||||
|
weight: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
});
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import { eq } from "drizzle-orm";
|
|||||||
import {
|
import {
|
||||||
sendDiscordNotification,
|
sendDiscordNotification,
|
||||||
sendEmailNotification,
|
sendEmailNotification,
|
||||||
sendLarkNotification,
|
|
||||||
sendGotifyNotification,
|
sendGotifyNotification,
|
||||||
|
sendLarkNotification,
|
||||||
sendNtfyNotification,
|
sendNtfyNotification,
|
||||||
sendSlackNotification,
|
sendSlackNotification,
|
||||||
sendTelegramNotification,
|
sendTelegramNotification,
|
||||||
@@ -34,18 +34,23 @@ export const sendDokployRestartNotifications = async () => {
|
|||||||
const { email, discord, telegram, slack, gotify, ntfy, lark } =
|
const { email, discord, telegram, slack, gotify, ntfy, lark } =
|
||||||
notification;
|
notification;
|
||||||
|
|
||||||
if (email) {
|
try {
|
||||||
const template = await renderAsync(
|
if (email) {
|
||||||
DokployRestartEmail({ date: date.toLocaleString() }),
|
const template = await renderAsync(
|
||||||
).catch();
|
DokployRestartEmail({ date: date.toLocaleString() }),
|
||||||
await sendEmailNotification(email, "Dokploy Server Restarted", template);
|
).catch();
|
||||||
}
|
|
||||||
|
|
||||||
if (discord) {
|
await sendEmailNotification(
|
||||||
const decorate = (decoration: string, text: string) =>
|
email,
|
||||||
`${discord.decoration ? decoration : ""} ${text}`.trim();
|
"Dokploy Server Restarted",
|
||||||
|
template,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (discord) {
|
||||||
|
const decorate = (decoration: string, text: string) =>
|
||||||
|
`${discord.decoration ? decoration : ""} ${text}`.trim();
|
||||||
|
|
||||||
try {
|
|
||||||
await sendDiscordNotification(discord, {
|
await sendDiscordNotification(discord, {
|
||||||
title: decorate(">", "`✅` Dokploy Server Restarted"),
|
title: decorate(">", "`✅` Dokploy Server Restarted"),
|
||||||
color: 0x57f287,
|
color: 0x57f287,
|
||||||
@@ -71,27 +76,19 @@ export const sendDokployRestartNotifications = async () => {
|
|||||||
text: "Dokploy Restart Notification",
|
text: "Dokploy Restart Notification",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (gotify) {
|
if (gotify) {
|
||||||
const decorate = (decoration: string, text: string) =>
|
const decorate = (decoration: string, text: string) =>
|
||||||
`${gotify.decoration ? decoration : ""} ${text}\n`;
|
`${gotify.decoration ? decoration : ""} ${text}\n`;
|
||||||
try {
|
|
||||||
await sendGotifyNotification(
|
await sendGotifyNotification(
|
||||||
gotify,
|
gotify,
|
||||||
decorate("✅", "Dokploy Server Restarted"),
|
decorate("✅", "Dokploy Server Restarted"),
|
||||||
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}`,
|
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}`,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (ntfy) {
|
if (ntfy) {
|
||||||
try {
|
|
||||||
await sendNtfyNotification(
|
await sendNtfyNotification(
|
||||||
ntfy,
|
ntfy,
|
||||||
"Dokploy Server Restarted",
|
"Dokploy Server Restarted",
|
||||||
@@ -99,25 +96,17 @@ export const sendDokployRestartNotifications = async () => {
|
|||||||
"",
|
"",
|
||||||
`🕒Date: ${date.toLocaleString()}`,
|
`🕒Date: ${date.toLocaleString()}`,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (telegram) {
|
if (telegram) {
|
||||||
try {
|
|
||||||
await sendTelegramNotification(
|
await sendTelegramNotification(
|
||||||
telegram,
|
telegram,
|
||||||
`<b>✅ Dokploy Server Restarted</b>\n\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`,
|
`<b>✅ Dokploy Server Restarted</b>\n\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (slack) {
|
if (slack) {
|
||||||
const { channel } = slack;
|
const { channel } = slack;
|
||||||
try {
|
|
||||||
await sendSlackNotification(slack, {
|
await sendSlackNotification(slack, {
|
||||||
channel: channel,
|
channel: channel,
|
||||||
attachments: [
|
attachments: [
|
||||||
@@ -134,13 +123,9 @@ export const sendDokployRestartNotifications = async () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (lark) {
|
if (lark) {
|
||||||
try {
|
|
||||||
await sendLarkNotification(lark, {
|
await sendLarkNotification(lark, {
|
||||||
msg_type: "interactive",
|
msg_type: "interactive",
|
||||||
card: {
|
card: {
|
||||||
@@ -182,7 +167,7 @@ export const sendDokployRestartNotifications = async () => {
|
|||||||
elements: [
|
elements: [
|
||||||
{
|
{
|
||||||
tag: "markdown",
|
tag: "markdown",
|
||||||
content: `**Status:**\nSuccessful`,
|
content: "**Status:**\nSuccessful",
|
||||||
text_align: "left",
|
text_align: "left",
|
||||||
text_size: "normal_v2",
|
text_size: "normal_v2",
|
||||||
},
|
},
|
||||||
@@ -210,9 +195,9 @@ export const sendDokployRestartNotifications = async () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type {
|
import type {
|
||||||
discord,
|
discord,
|
||||||
email,
|
email,
|
||||||
lark,
|
|
||||||
gotify,
|
gotify,
|
||||||
|
lark,
|
||||||
ntfy,
|
ntfy,
|
||||||
slack,
|
slack,
|
||||||
telegram,
|
telegram,
|
||||||
@@ -38,6 +38,9 @@ export const sendEmailNotification = async (
|
|||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
|
throw new Error(
|
||||||
|
`Failed to send email notification ${err instanceof Error ? err.message : "Unknown error"}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,15 +48,23 @@ export const sendDiscordNotification = async (
|
|||||||
connection: typeof discord.$inferInsert,
|
connection: typeof discord.$inferInsert,
|
||||||
embed: any,
|
embed: any,
|
||||||
) => {
|
) => {
|
||||||
// try {
|
try {
|
||||||
await fetch(connection.webhookUrl, {
|
const response = await fetch(connection.webhookUrl, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ embeds: [embed] }),
|
body: JSON.stringify({ embeds: [embed] }),
|
||||||
});
|
});
|
||||||
// } catch (err) {
|
if (!response.ok) {
|
||||||
// console.log(err);
|
throw new Error(
|
||||||
// }
|
`Failed to send discord notification ${response.statusText}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log("error", err);
|
||||||
|
throw new Error(
|
||||||
|
`Failed to send discord notification ${err instanceof Error ? err.message : "Unknown error"}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sendTelegramNotification = async (
|
export const sendTelegramNotification = async (
|
||||||
@@ -90,13 +101,21 @@ export const sendSlackNotification = async (
|
|||||||
message: any,
|
message: any,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
await fetch(connection.webhookUrl, {
|
const response = await fetch(connection.webhookUrl, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify(message),
|
body: JSON.stringify(message),
|
||||||
});
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to send slack notification ${response.statusText}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log("error", err);
|
||||||
|
throw new Error(
|
||||||
|
`Failed to send slack notification ${err instanceof Error ? err.message : "Unknown error"}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
55
packages/server/src/utils/process/ExecError.ts
Normal file
55
packages/server/src/utils/process/ExecError.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
export interface ExecErrorDetails {
|
||||||
|
command: string;
|
||||||
|
stdout?: string;
|
||||||
|
stderr?: string;
|
||||||
|
exitCode?: number;
|
||||||
|
originalError?: Error;
|
||||||
|
serverId?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ExecError extends Error {
|
||||||
|
public readonly command: string;
|
||||||
|
public readonly stdout?: string;
|
||||||
|
public readonly stderr?: string;
|
||||||
|
public readonly exitCode?: number;
|
||||||
|
public readonly originalError?: Error;
|
||||||
|
public readonly serverId?: string | null;
|
||||||
|
|
||||||
|
constructor(message: string, details: ExecErrorDetails) {
|
||||||
|
super(message);
|
||||||
|
this.name = "ExecError";
|
||||||
|
this.command = details.command;
|
||||||
|
this.stdout = details.stdout;
|
||||||
|
this.stderr = details.stderr;
|
||||||
|
this.exitCode = details.exitCode;
|
||||||
|
this.originalError = details.originalError;
|
||||||
|
this.serverId = details.serverId;
|
||||||
|
|
||||||
|
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
||||||
|
if (Error.captureStackTrace) {
|
||||||
|
Error.captureStackTrace(this, ExecError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a formatted error message with all details
|
||||||
|
*/
|
||||||
|
getDetailedMessage(): string {
|
||||||
|
const parts = [
|
||||||
|
`Command: ${this.command}`,
|
||||||
|
this.exitCode !== undefined ? `Exit Code: ${this.exitCode}` : null,
|
||||||
|
this.serverId ? `Server ID: ${this.serverId}` : "Location: Local",
|
||||||
|
this.stderr ? `Stderr: ${this.stderr}` : null,
|
||||||
|
this.stdout ? `Stdout: ${this.stdout}` : null,
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
return `${this.message}\n${parts.join("\n")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this error is from a remote execution
|
||||||
|
*/
|
||||||
|
isRemote(): boolean {
|
||||||
|
return !!this.serverId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,8 +2,43 @@ import { exec, execFile } from "node:child_process";
|
|||||||
import util from "node:util";
|
import util from "node:util";
|
||||||
import { findServerById } from "@dokploy/server/services/server";
|
import { findServerById } from "@dokploy/server/services/server";
|
||||||
import { Client } from "ssh2";
|
import { Client } from "ssh2";
|
||||||
|
import { ExecError } from "./ExecError";
|
||||||
|
|
||||||
export const execAsync = util.promisify(exec);
|
// Re-export ExecError for easier imports
|
||||||
|
export { ExecError } from "./ExecError";
|
||||||
|
|
||||||
|
const execAsyncBase = util.promisify(exec);
|
||||||
|
|
||||||
|
export const execAsync = async (
|
||||||
|
command: string,
|
||||||
|
options?: { cwd?: string; env?: NodeJS.ProcessEnv; shell?: string },
|
||||||
|
): Promise<{ stdout: string; stderr: string }> => {
|
||||||
|
try {
|
||||||
|
const result = await execAsyncBase(command, options);
|
||||||
|
return {
|
||||||
|
stdout: result.stdout.toString(),
|
||||||
|
stderr: result.stderr.toString(),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
// @ts-ignore - exec error has these properties
|
||||||
|
const exitCode = error.code;
|
||||||
|
// @ts-ignore
|
||||||
|
const stdout = error.stdout?.toString() || "";
|
||||||
|
// @ts-ignore
|
||||||
|
const stderr = error.stderr?.toString() || "";
|
||||||
|
|
||||||
|
throw new ExecError(`Command execution failed: ${error.message}`, {
|
||||||
|
command,
|
||||||
|
stdout,
|
||||||
|
stderr,
|
||||||
|
exitCode,
|
||||||
|
originalError: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
interface ExecOptions {
|
interface ExecOptions {
|
||||||
cwd?: string;
|
cwd?: string;
|
||||||
@@ -21,7 +56,16 @@ export const execAsyncStream = (
|
|||||||
|
|
||||||
const childProcess = exec(command, options, (error) => {
|
const childProcess = exec(command, options, (error) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
reject(error);
|
reject(
|
||||||
|
new ExecError(`Command execution failed: ${error.message}`, {
|
||||||
|
command,
|
||||||
|
stdout: stdoutComplete,
|
||||||
|
stderr: stderrComplete,
|
||||||
|
// @ts-ignore
|
||||||
|
exitCode: error.code,
|
||||||
|
originalError: error,
|
||||||
|
}),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
resolve({ stdout: stdoutComplete, stderr: stderrComplete });
|
resolve({ stdout: stdoutComplete, stderr: stderrComplete });
|
||||||
@@ -45,7 +89,14 @@ export const execAsyncStream = (
|
|||||||
|
|
||||||
childProcess.on("error", (error) => {
|
childProcess.on("error", (error) => {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
reject(error);
|
reject(
|
||||||
|
new ExecError(`Command execution error: ${error.message}`, {
|
||||||
|
command,
|
||||||
|
stdout: stdoutComplete,
|
||||||
|
stderr: stderrComplete,
|
||||||
|
originalError: error,
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -108,7 +159,14 @@ export const execAsyncRemote = async (
|
|||||||
conn.exec(command, (err, stream) => {
|
conn.exec(command, (err, stream) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
onData?.(err.message);
|
onData?.(err.message);
|
||||||
throw err;
|
reject(
|
||||||
|
new ExecError(`Remote command execution failed: ${err.message}`, {
|
||||||
|
command,
|
||||||
|
serverId,
|
||||||
|
originalError: err,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
stream
|
stream
|
||||||
.on("close", (code: number, _signal: string) => {
|
.on("close", (code: number, _signal: string) => {
|
||||||
@@ -116,7 +174,18 @@ export const execAsyncRemote = async (
|
|||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
resolve({ stdout, stderr });
|
resolve({ stdout, stderr });
|
||||||
} else {
|
} else {
|
||||||
reject(new Error(`Error occurred ❌: ${stderr}`));
|
reject(
|
||||||
|
new ExecError(
|
||||||
|
`Remote command failed with exit code ${code}`,
|
||||||
|
{
|
||||||
|
command,
|
||||||
|
stdout,
|
||||||
|
stderr,
|
||||||
|
exitCode: code,
|
||||||
|
serverId,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on("data", (data: string) => {
|
.on("data", (data: string) => {
|
||||||
@@ -132,17 +201,25 @@ export const execAsyncRemote = async (
|
|||||||
.on("error", (err) => {
|
.on("error", (err) => {
|
||||||
conn.end();
|
conn.end();
|
||||||
if (err.level === "client-authentication") {
|
if (err.level === "client-authentication") {
|
||||||
onData?.(
|
const errorMsg = `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`;
|
||||||
`Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`,
|
onData?.(errorMsg);
|
||||||
);
|
|
||||||
reject(
|
reject(
|
||||||
new Error(
|
new ExecError(errorMsg, {
|
||||||
`Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`,
|
command,
|
||||||
),
|
serverId,
|
||||||
|
originalError: err,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
onData?.(`SSH connection error: ${err.message}`);
|
const errorMsg = `SSH connection error: ${err.message}`;
|
||||||
reject(new Error(`SSH connection error: ${err.message}`));
|
onData?.(errorMsg);
|
||||||
|
reject(
|
||||||
|
new ExecError(errorMsg, {
|
||||||
|
command,
|
||||||
|
serverId,
|
||||||
|
originalError: err,
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.connect({
|
.connect({
|
||||||
|
|||||||
@@ -159,7 +159,6 @@ export const cloneGithubRepository = async ({
|
|||||||
const octokit = authGithub(githubProvider);
|
const octokit = authGithub(githubProvider);
|
||||||
const token = await getGithubToken(octokit);
|
const token = await getGithubToken(octokit);
|
||||||
const repoclone = `github.com/${owner}/${repository}.git`;
|
const repoclone = `github.com/${owner}/${repository}.git`;
|
||||||
// await recreateDirectory(outputPath);
|
|
||||||
command += `rm -rf ${outputPath};`;
|
command += `rm -rf ${outputPath};`;
|
||||||
command += `mkdir -p ${outputPath};`;
|
command += `mkdir -p ${outputPath};`;
|
||||||
const cloneUrl = `https://oauth2:${token}@${repoclone}`;
|
const cloneUrl = `https://oauth2:${token}@${repoclone}`;
|
||||||
|
|||||||
@@ -290,7 +290,7 @@ export const validateGitlabProvider = async (gitlabProvider: Gitlab) => {
|
|||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${gitlabProvider.gitlabUrl}/api/v4/projects?membership=true&owned=true&page=${page}&per_page=${perPage}`,
|
`${gitlabProvider.gitlabUrl}/api/v4/projects?membership=true&page=${page}&per_page=${perPage}`,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${gitlabProvider.accessToken}`,
|
Authorization: `Bearer ${gitlabProvider.accessToken}`,
|
||||||
|
|||||||
17
pnpm-lock.yaml
generated
17
pnpm-lock.yaml
generated
@@ -406,6 +406,9 @@ importers:
|
|||||||
rotating-file-stream:
|
rotating-file-stream:
|
||||||
specifier: 3.2.3
|
specifier: 3.2.3
|
||||||
version: 3.2.3
|
version: 3.2.3
|
||||||
|
shell-quote:
|
||||||
|
specifier: ^1.8.1
|
||||||
|
version: 1.8.2
|
||||||
slugify:
|
slugify:
|
||||||
specifier: ^1.6.6
|
specifier: ^1.6.6
|
||||||
version: 1.6.6
|
version: 1.6.6
|
||||||
@@ -488,6 +491,9 @@ importers:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
specifier: 18.3.0
|
specifier: 18.3.0
|
||||||
version: 18.3.0
|
version: 18.3.0
|
||||||
|
'@types/shell-quote':
|
||||||
|
specifier: ^1.7.5
|
||||||
|
version: 1.7.5
|
||||||
'@types/ssh2':
|
'@types/ssh2':
|
||||||
specifier: 1.15.1
|
specifier: 1.15.1
|
||||||
version: 1.15.1
|
version: 1.15.1
|
||||||
@@ -726,6 +732,9 @@ importers:
|
|||||||
rotating-file-stream:
|
rotating-file-stream:
|
||||||
specifier: 3.2.3
|
specifier: 3.2.3
|
||||||
version: 3.2.3
|
version: 3.2.3
|
||||||
|
shell-quote:
|
||||||
|
specifier: ^1.8.1
|
||||||
|
version: 1.8.2
|
||||||
slugify:
|
slugify:
|
||||||
specifier: ^1.6.6
|
specifier: ^1.6.6
|
||||||
version: 1.6.6
|
version: 1.6.6
|
||||||
@@ -778,6 +787,9 @@ importers:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
specifier: 18.3.0
|
specifier: 18.3.0
|
||||||
version: 18.3.0
|
version: 18.3.0
|
||||||
|
'@types/shell-quote':
|
||||||
|
specifier: ^1.7.5
|
||||||
|
version: 1.7.5
|
||||||
'@types/ssh2':
|
'@types/ssh2':
|
||||||
specifier: 1.15.1
|
specifier: 1.15.1
|
||||||
version: 1.15.1
|
version: 1.15.1
|
||||||
@@ -4033,6 +4045,9 @@ packages:
|
|||||||
'@types/readable-stream@4.0.20':
|
'@types/readable-stream@4.0.20':
|
||||||
resolution: {integrity: sha512-eLgbR5KwUh8+6pngBDxS32MymdCsCHnGtwHTrC0GDorbc7NbcnkZAWptDLgZiRk9VRas+B6TyRgPDucq4zRs8g==}
|
resolution: {integrity: sha512-eLgbR5KwUh8+6pngBDxS32MymdCsCHnGtwHTrC0GDorbc7NbcnkZAWptDLgZiRk9VRas+B6TyRgPDucq4zRs8g==}
|
||||||
|
|
||||||
|
'@types/shell-quote@1.7.5':
|
||||||
|
resolution: {integrity: sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw==}
|
||||||
|
|
||||||
'@types/shimmer@1.2.0':
|
'@types/shimmer@1.2.0':
|
||||||
resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==}
|
resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==}
|
||||||
|
|
||||||
@@ -11383,6 +11398,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 20.17.51
|
'@types/node': 20.17.51
|
||||||
|
|
||||||
|
'@types/shell-quote@1.7.5': {}
|
||||||
|
|
||||||
'@types/shimmer@1.2.0': {}
|
'@types/shimmer@1.2.0': {}
|
||||||
|
|
||||||
'@types/ssh2@1.15.1':
|
'@types/ssh2@1.15.1':
|
||||||
|
|||||||
Reference in New Issue
Block a user