mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-11 08:55:22 +02:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5abd46386 | ||
|
|
ad0e044740 | ||
|
|
7a0ff72f51 | ||
|
|
2e702dc41f | ||
|
|
766f9244da | ||
|
|
6413fa54e6 | ||
|
|
1c9dcc0c9e | ||
|
|
fee802a57b | ||
|
|
af2b053caa | ||
|
|
42a4cc7fff | ||
|
|
2a7807c2b3 | ||
|
|
425b8ec3c2 | ||
|
|
96dff0c1bb | ||
|
|
f53e1a6543 | ||
|
|
9e2788e764 | ||
|
|
4884ee3352 | ||
|
|
3a17c9b9e8 | ||
|
|
63568a4887 |
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é");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { Scissors } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
id: string;
|
||||||
|
type: "application" | "compose";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const KillBuild = ({ id, type }: Props) => {
|
||||||
|
const { mutateAsync, isLoading } =
|
||||||
|
type === "application"
|
||||||
|
? api.application.killBuild.useMutation()
|
||||||
|
: api.compose.killBuild.useMutation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button variant="outline" className="w-fit" isLoading={isLoading}>
|
||||||
|
Kill Build
|
||||||
|
<Scissors className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Are you sure to kill the build?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This will kill the build process
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={async () => {
|
||||||
|
await mutateAsync({
|
||||||
|
applicationId: id || "",
|
||||||
|
composeId: id || "",
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Build killed successfully");
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error(err.message);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
import { api, type RouterOutputs } from "@/utils/api";
|
import { api, type RouterOutputs } from "@/utils/api";
|
||||||
import { ShowRollbackSettings } from "../rollbacks/show-rollback-settings";
|
import { ShowRollbackSettings } from "../rollbacks/show-rollback-settings";
|
||||||
import { CancelQueues } from "./cancel-queues";
|
import { CancelQueues } from "./cancel-queues";
|
||||||
|
import { KillBuild } from "./kill-build";
|
||||||
import { RefreshToken } from "./refresh-token";
|
import { RefreshToken } from "./refresh-token";
|
||||||
import { ShowDeployment } from "./show-deployment";
|
import { ShowDeployment } from "./show-deployment";
|
||||||
|
|
||||||
@@ -143,6 +144,9 @@ export const ShowDeployments = ({
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row items-center gap-2">
|
<div className="flex flex-row items-center gap-2">
|
||||||
|
{(type === "application" || type === "compose") && (
|
||||||
|
<KillBuild id={id} type={type} />
|
||||||
|
)}
|
||||||
{(type === "application" || type === "compose") && (
|
{(type === "application" || type === "compose") && (
|
||||||
<CancelQueues id={id} type={type} />
|
<CancelQueues id={id} type={type} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.25.7",
|
"version": "v0.25.10",
|
||||||
"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",
|
||||||
|
|||||||
@@ -58,7 +58,11 @@ import {
|
|||||||
applications,
|
applications,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import type { DeploymentJob } from "@/server/queues/queue-types";
|
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||||
import { cleanQueuesByApplication, myQueue } from "@/server/queues/queueSetup";
|
import {
|
||||||
|
cleanQueuesByApplication,
|
||||||
|
killDockerBuild,
|
||||||
|
myQueue,
|
||||||
|
} from "@/server/queues/queueSetup";
|
||||||
import { cancelDeployment, deploy } from "@/server/utils/deploy";
|
import { cancelDeployment, deploy } from "@/server/utils/deploy";
|
||||||
import { uploadFileSchema } from "@/utils/schema";
|
import { uploadFileSchema } from "@/utils/schema";
|
||||||
|
|
||||||
@@ -725,7 +729,21 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
await cleanQueuesByApplication(input.applicationId);
|
await cleanQueuesByApplication(input.applicationId);
|
||||||
}),
|
}),
|
||||||
|
killBuild: protectedProcedure
|
||||||
|
.input(apiFindOneApplication)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const application = await findApplicationById(input.applicationId);
|
||||||
|
if (
|
||||||
|
application.environment.project.organizationId !==
|
||||||
|
ctx.session.activeOrganizationId
|
||||||
|
) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not authorized to kill this build",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await killDockerBuild("application", application.serverId);
|
||||||
|
}),
|
||||||
readTraefikConfig: protectedProcedure
|
readTraefikConfig: protectedProcedure
|
||||||
.input(apiFindOneApplication)
|
.input(apiFindOneApplication)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
|
|||||||
@@ -59,7 +59,11 @@ import {
|
|||||||
compose as composeTable,
|
compose as composeTable,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import type { DeploymentJob } from "@/server/queues/queue-types";
|
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||||
import { cleanQueuesByCompose, myQueue } from "@/server/queues/queueSetup";
|
import {
|
||||||
|
cleanQueuesByCompose,
|
||||||
|
killDockerBuild,
|
||||||
|
myQueue,
|
||||||
|
} from "@/server/queues/queueSetup";
|
||||||
import { cancelDeployment, deploy } from "@/server/utils/deploy";
|
import { cancelDeployment, deploy } from "@/server/utils/deploy";
|
||||||
import { generatePassword } from "@/templates/utils";
|
import { generatePassword } from "@/templates/utils";
|
||||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
||||||
@@ -248,6 +252,21 @@ export const composeRouter = createTRPCRouter({
|
|||||||
await cleanQueuesByCompose(input.composeId);
|
await cleanQueuesByCompose(input.composeId);
|
||||||
return { success: true, message: "Queues cleaned successfully" };
|
return { success: true, message: "Queues cleaned successfully" };
|
||||||
}),
|
}),
|
||||||
|
killBuild: protectedProcedure
|
||||||
|
.input(apiFindCompose)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const compose = await findComposeById(input.composeId);
|
||||||
|
if (
|
||||||
|
compose.environment.project.organizationId !==
|
||||||
|
ctx.session.activeOrganizationId
|
||||||
|
) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not authorized to kill this build",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await killDockerBuild("compose", compose.serverId);
|
||||||
|
}),
|
||||||
|
|
||||||
loadServices: protectedProcedure
|
loadServices: protectedProcedure
|
||||||
.input(apiFetchServices)
|
.input(apiFetchServices)
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import {
|
||||||
|
execAsync,
|
||||||
|
execAsyncRemote,
|
||||||
|
} from "@dokploy/server/utils/process/execAsync";
|
||||||
import { Queue } from "bullmq";
|
import { Queue } from "bullmq";
|
||||||
import { redisConfig } from "./redis-connection";
|
import { redisConfig } from "./redis-connection";
|
||||||
|
|
||||||
@@ -41,4 +45,31 @@ export const cleanQueuesByCompose = async (composeId: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const killDockerBuild = async (
|
||||||
|
type: "application" | "compose",
|
||||||
|
serverId: string | null,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
if (type === "application") {
|
||||||
|
const command = `pkill -2 -f "docker build"`;
|
||||||
|
|
||||||
|
if (serverId) {
|
||||||
|
await execAsyncRemote(serverId, command);
|
||||||
|
} else {
|
||||||
|
await execAsync(command);
|
||||||
|
}
|
||||||
|
} else if (type === "compose") {
|
||||||
|
const command = `pkill -2 -f "docker compose"`;
|
||||||
|
|
||||||
|
if (serverId) {
|
||||||
|
await execAsyncRemote(serverId, command);
|
||||||
|
} else {
|
||||||
|
await execAsync(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export { myQueue };
|
export { myQueue };
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -225,6 +225,7 @@ 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};`;
|
const command = `echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath};`;
|
||||||
@@ -273,6 +274,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,6 +295,27 @@ 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) {
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||||
await updateApplicationStatus(applicationId, "error");
|
await updateApplicationStatus(applicationId, "error");
|
||||||
|
|||||||
@@ -267,6 +267,7 @@ 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) {
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||||
@@ -375,7 +376,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 +403,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 +438,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),
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ 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 { writeDomainsToComposeRemote } from "../docker/domain";
|
import { quote } from "shell-quote";
|
||||||
|
import { writeDomainsToCompose } from "../docker/domain";
|
||||||
import {
|
import {
|
||||||
encodeBase64,
|
encodeBase64,
|
||||||
getEnviromentVariablesObject,
|
getEnviromentVariablesObject,
|
||||||
@@ -22,7 +23,7 @@ export const getBuildComposeCommand = async (compose: ComposeNested) => {
|
|||||||
const projectPath = join(COMPOSE_PATH, compose.appName, "code");
|
const projectPath = join(COMPOSE_PATH, compose.appName, "code");
|
||||||
const exportEnvCommand = getExportEnvCommand(compose);
|
const exportEnvCommand = getExportEnvCommand(compose);
|
||||||
|
|
||||||
const newCompose = await writeDomainsToComposeRemote(compose, domains);
|
const newCompose = await writeDomainsToCompose(compose, domains);
|
||||||
const logContent = `
|
const logContent = `
|
||||||
App Name: ${appName}
|
App Name: ${appName}
|
||||||
Build Compose 🐳
|
Build Compose 🐳
|
||||||
@@ -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])}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ export const readComposeFile = async (compose: Compose) => {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const writeDomainsToComposeRemote = async (
|
export const writeDomainsToCompose = async (
|
||||||
compose: Compose,
|
compose: Compose,
|
||||||
domains: Domain[],
|
domains: Domain[],
|
||||||
) => {
|
) => {
|
||||||
@@ -120,19 +120,16 @@ echo "❌ Error: Compose file not found";
|
|||||||
exit 1;
|
exit 1;
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
if (compose.serverId) {
|
|
||||||
const composeString = stringify(composeConverted, { lineWidth: 1000 });
|
const composeString = stringify(composeConverted, { lineWidth: 1000 });
|
||||||
const encodedContent = encodeBase64(composeString);
|
const encodedContent = encodeBase64(composeString);
|
||||||
return `echo "${encodedContent}" | base64 -d > "${path}";`;
|
return `echo "${encodedContent}" | base64 -d > "${path}";`;
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return `echo "❌ Has occured an error: ${error?.message || error}";
|
return `echo "❌ Has occurred an error: ${error?.message || error}";
|
||||||
exit 1;
|
exit 1;
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return "";
|
|
||||||
};
|
};
|
||||||
export const addDomainToCompose = async (
|
export const addDomainToCompose = async (
|
||||||
compose: Compose,
|
compose: Compose,
|
||||||
|
|||||||
@@ -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] => {
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -62,6 +64,7 @@ export const sendBuildSuccessNotifications = async ({
|
|||||||
applicationType,
|
applicationType,
|
||||||
buildLink,
|
buildLink,
|
||||||
date: date.toLocaleString(),
|
date: date.toLocaleString(),
|
||||||
|
environmentName,
|
||||||
}),
|
}),
|
||||||
).catch();
|
).catch();
|
||||||
await sendEmailNotification(email, "Build success for dokploy", template);
|
await sendEmailNotification(email, "Build success for dokploy", template);
|
||||||
@@ -72,7 +75,7 @@ export const sendBuildSuccessNotifications = async ({
|
|||||||
`${discord.decoration ? decoration : ""} ${text}`.trim();
|
`${discord.decoration ? decoration : ""} ${text}`.trim();
|
||||||
|
|
||||||
await sendDiscordNotification(discord, {
|
await sendDiscordNotification(discord, {
|
||||||
title: decorate(">", "`✅` Build Success"),
|
title: decorate(">", "`✅` Build Successes"),
|
||||||
color: 0x57f287,
|
color: 0x57f287,
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
@@ -85,6 +88,11 @@ export const sendBuildSuccessNotifications = async ({
|
|||||||
value: applicationName,
|
value: applicationName,
|
||||||
inline: true,
|
inline: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: decorate("`🌍`", "Environment"),
|
||||||
|
value: environmentName,
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: decorate("`❔`", "Type"),
|
name: decorate("`❔`", "Type"),
|
||||||
value: applicationType,
|
value: applicationType,
|
||||||
@@ -125,6 +133,7 @@ export const sendBuildSuccessNotifications = async ({
|
|||||||
decorate("✅", "Build Success"),
|
decorate("✅", "Build Success"),
|
||||||
`${decorate("🛠️", `Project: ${projectName}`)}` +
|
`${decorate("🛠️", `Project: ${projectName}`)}` +
|
||||||
`${decorate("⚙️", `Application: ${applicationName}`)}` +
|
`${decorate("⚙️", `Application: ${applicationName}`)}` +
|
||||||
|
`${decorate("🌍", `Environment: ${environmentName}`)}` +
|
||||||
`${decorate("❔", `Type: ${applicationType}`)}` +
|
`${decorate("❔", `Type: ${applicationType}`)}` +
|
||||||
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
|
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
|
||||||
`${decorate("🔗", `Build details:\n${buildLink}`)}`,
|
`${decorate("🔗", `Build details:\n${buildLink}`)}`,
|
||||||
@@ -139,6 +148,7 @@ export const sendBuildSuccessNotifications = async ({
|
|||||||
`view, Build details, ${buildLink}, clear=true;`,
|
`view, Build details, ${buildLink}, clear=true;`,
|
||||||
`🛠Project: ${projectName}\n` +
|
`🛠Project: ${projectName}\n` +
|
||||||
`⚙️Application: ${applicationName}\n` +
|
`⚙️Application: ${applicationName}\n` +
|
||||||
|
`🌍Environment: ${environmentName}\n` +
|
||||||
`❔Type: ${applicationType}\n` +
|
`❔Type: ${applicationType}\n` +
|
||||||
`🕒Date: ${date.toLocaleString()}`,
|
`🕒Date: ${date.toLocaleString()}`,
|
||||||
);
|
);
|
||||||
@@ -167,7 +177,7 @@ export const sendBuildSuccessNotifications = async ({
|
|||||||
|
|
||||||
await sendTelegramNotification(
|
await sendTelegramNotification(
|
||||||
telegram,
|
telegram,
|
||||||
`<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")}`,
|
`<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,
|
inlineButton,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -191,6 +201,11 @@ export const sendBuildSuccessNotifications = async ({
|
|||||||
value: applicationName,
|
value: applicationName,
|
||||||
short: true,
|
short: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Environment",
|
||||||
|
value: environmentName,
|
||||||
|
short: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Type",
|
title: "Type",
|
||||||
value: applicationType,
|
value: applicationType,
|
||||||
@@ -260,6 +275,12 @@ export const sendBuildSuccessNotifications = async ({
|
|||||||
text_align: "left",
|
text_align: "left",
|
||||||
text_size: "normal_v2",
|
text_size: "normal_v2",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Environment:**\n${environmentName}`,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
tag: "markdown",
|
tag: "markdown",
|
||||||
content: `**Type:**\n${applicationType}`,
|
content: `**Type:**\n${applicationType}`,
|
||||||
|
|||||||
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