diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 0b849afc0..d45c3dac0 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -6,9 +6,9 @@ Please describe in a short paragraph what this PR is about. Before submitting this PR, please make sure that: -- [] You created a dedicated branch based on the `canary` branch. -- [] You have read the suggestions in the CONTRIBUTING.md file https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#pull-request -- [] You have tested this PR in your local instance. +- [ ] You created a dedicated branch based on the `canary` branch. +- [ ] You have read the suggestions in the CONTRIBUTING.md file https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#pull-request +- [ ] You have tested this PR in your local instance. ## Issues related (if applicable) diff --git a/Dockerfile b/Dockerfile index 11310b18e..ae8c997f8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -46,7 +46,7 @@ COPY --from=build /prod/dokploy/node_modules ./node_modules # 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 # | VERBOSE=1 VERSION=1.21.0 bash diff --git a/apps/api/src/utils.ts b/apps/api/src/utils.ts index ee2ac3e50..0d0b574fc 100644 --- a/apps/api/src/utils.ts +++ b/apps/api/src/utils.ts @@ -1,9 +1,9 @@ import { - deployRemoteApplication, - deployRemoteCompose, - deployRemotePreviewApplication, - rebuildRemoteApplication, - rebuildRemoteCompose, + deployApplication, + deployCompose, + deployPreviewApplication, + rebuildApplication, + rebuildCompose, updateApplicationStatus, updateCompose, updatePreviewDeployment, @@ -16,13 +16,13 @@ export const deploy = async (job: DeployJob) => { await updateApplicationStatus(job.applicationId, "running"); if (job.server) { if (job.type === "redeploy") { - await rebuildRemoteApplication({ + await rebuildApplication({ applicationId: job.applicationId, titleLog: job.titleLog || "Rebuild deployment", descriptionLog: job.descriptionLog || "", }); } else if (job.type === "deploy") { - await deployRemoteApplication({ + await deployApplication({ applicationId: job.applicationId, titleLog: job.titleLog || "Manual deployment", descriptionLog: job.descriptionLog || "", @@ -36,13 +36,13 @@ export const deploy = async (job: DeployJob) => { if (job.server) { if (job.type === "redeploy") { - await rebuildRemoteCompose({ + await rebuildCompose({ composeId: job.composeId, titleLog: job.titleLog || "Rebuild deployment", descriptionLog: job.descriptionLog || "", }); } else if (job.type === "deploy") { - await deployRemoteCompose({ + await deployCompose({ composeId: job.composeId, titleLog: job.titleLog || "Manual deployment", descriptionLog: job.descriptionLog || "", @@ -55,7 +55,7 @@ export const deploy = async (job: DeployJob) => { }); if (job.server) { if (job.type === "deploy") { - await deployRemotePreviewApplication({ + await deployPreviewApplication({ applicationId: job.applicationId, titleLog: job.titleLog || "Preview Deployment", descriptionLog: job.descriptionLog || "", diff --git a/apps/dokploy/.env.example b/apps/dokploy/.env.example index ba57ec7be..8f801196e 100644 --- a/apps/dokploy/.env.example +++ b/apps/dokploy/.env.example @@ -1,3 +1,3 @@ DATABASE_URL="postgres://dokploy:amukds4wi9001583845717ad2@localhost:5432/dokploy" PORT=3000 -NODE_ENV=development \ No newline at end of file +NODE_ENV=development diff --git a/apps/dokploy/__test__/deploy/github.test.ts b/apps/dokploy/__test__/deploy/github.test.ts index 03805b08d..46be44883 100644 --- a/apps/dokploy/__test__/deploy/github.test.ts +++ b/apps/dokploy/__test__/deploy/github.test.ts @@ -1,5 +1,10 @@ import { describe, expect, it } from "vitest"; -import { extractCommitMessage } from "@/pages/api/deploy/[refreshToken]"; +import { + extractCommitMessage, + extractImageName, + extractImageTag, + extractImageTagFromRequest, +} from "@/pages/api/deploy/[refreshToken]"; describe("GitHub Webhook Skip CI", () => { const mockGithubHeaders = { @@ -96,3 +101,308 @@ describe("GitHub Webhook Skip CI", () => { ); }); }); + +describe("GitHub Packages Docker Image Tag Extraction", () => { + it("should extract tag from container_metadata", () => { + const headers = { "x-github-event": "registry_package" }; + const body = { + registry_package: { + package_version: { + version: "sha256:abc123...", + container_metadata: { + tag: { + name: "v1.0.0", + digest: "sha256:abc123...", + }, + }, + package_url: "ghcr.io/owner/repo:v1.0.0", + }, + }, + }; + + const tag = extractImageTagFromRequest(headers, body); + expect(tag).toBe("v1.0.0"); + }); + + it("should extract tag from package_url when container_metadata tag matches version", () => { + const headers = { "x-github-event": "registry_package" }; + const body = { + registry_package: { + package_version: { + version: "sha256:abc123...", + container_metadata: { + tag: { + name: "sha256:abc123...", + digest: "sha256:abc123...", + }, + }, + package_url: "ghcr.io/owner/repo:latest", + }, + }, + }; + + const tag = extractImageTagFromRequest(headers, body); + expect(tag).toBe("latest"); + }); + + it("should extract tag from package_url when container_metadata is missing", () => { + const headers = { "x-github-event": "registry_package" }; + const body = { + registry_package: { + package_version: { + version: "sha256:abc123...", + package_url: "ghcr.io/owner/repo:1.2.3", + }, + }, + }; + + const tag = extractImageTagFromRequest(headers, body); + expect(tag).toBe("1.2.3"); + }); + + it("should handle different tag formats in package_url", () => { + const headers = { "x-github-event": "registry_package" }; + const testCases = [ + { url: "ghcr.io/owner/repo:latest", expected: "latest" }, + { url: "ghcr.io/owner/repo:v1.0.0", expected: "v1.0.0" }, + { url: "ghcr.io/owner/repo:1.2.3", expected: "1.2.3" }, + { url: "ghcr.io/owner/repo:dev", expected: "dev" }, + ]; + + for (const testCase of testCases) { + const body = { + registry_package: { + package_version: { + version: "sha256:abc123...", + package_url: testCase.url, + }, + }, + }; + + const tag = extractImageTagFromRequest(headers, body); + expect(tag).toBe(testCase.expected); + } + }); + + it("should return null for non-registry_package events", () => { + const headers = { "x-github-event": "push" }; + const body = { + registry_package: { + package_version: { + package_url: "ghcr.io/owner/repo:latest", + }, + }, + }; + + const tag = extractImageTagFromRequest(headers, body); + expect(tag).toBeNull(); + }); + + it("should return null when package_version is missing", () => { + const headers = { "x-github-event": "registry_package" }; + const body = { + registry_package: {}, + }; + + const tag = extractImageTagFromRequest(headers, body); + expect(tag).toBeNull(); + }); + + it("should return null when package_url has no tag", () => { + const headers = { "x-github-event": "registry_package" }; + const body = { + registry_package: { + package_version: { + version: "sha256:abc123...", + package_url: "ghcr.io/owner/repo", + }, + }, + }; + + const tag = extractImageTagFromRequest(headers, body); + expect(tag).toBeNull(); + }); + + it("should return null when package_url ends with colon (no tag)", () => { + const headers = { "x-github-event": "registry_package" }; + const body = { + registry_package: { + package_version: { + version: "sha256:abc123...", + package_url: "ghcr.io/owner/repo:", + container_metadata: { + tag: { + name: "", + digest: "sha256:abc123...", + }, + }, + }, + }, + }; + + const tag = extractImageTagFromRequest(headers, body); + expect(tag).toBeNull(); + }); + + it("should return null when tag name is empty string", () => { + const headers = { "x-github-event": "registry_package" }; + const body = { + registry_package: { + package_version: { + version: "sha256:abc123...", + container_metadata: { + tag: { + name: "", + digest: "sha256:abc123...", + }, + }, + package_url: "ghcr.io/owner/repo:", + }, + }, + }; + + const tag = extractImageTagFromRequest(headers, body); + expect(tag).toBeNull(); + }); + + it("should ignore tag if it matches the version (digest)", () => { + const headers = { "x-github-event": "registry_package" }; + const body = { + registry_package: { + package_version: { + version: "sha256:abc123...", + container_metadata: { + tag: { + name: "sha256:abc123...", + digest: "sha256:abc123...", + }, + }, + package_url: "ghcr.io/owner/repo:latest", + }, + }, + }; + + const tag = extractImageTagFromRequest(headers, body); + expect(tag).toBe("latest"); + }); + + it("should handle registry_package commit message with package_url", () => { + const headers = { "x-github-event": "registry_package" }; + const body = { + registry_package: { + package_version: { + package_url: "ghcr.io/owner/repo:latest", + }, + }, + }; + + const message = extractCommitMessage(headers, body); + expect(message).toBe("Docker GHCR image pushed: ghcr.io/owner/repo:latest"); + }); + + it("should handle registry_package commit message when package_url is missing", () => { + const headers = { "x-github-event": "registry_package" }; + const body = { + registry_package: { + package_version: { + version: "sha256:abc123...", + }, + }, + }; + + const message = extractCommitMessage(headers, body); + expect(message).toBe("Docker GHCR image pushed"); + }); + + it("should handle registry_package commit message when package_version is missing", () => { + const headers = { "x-github-event": "registry_package" }; + const body = { + registry_package: {}, + }; + + const message = extractCommitMessage(headers, body); + expect(message).toBe("NEW COMMIT"); + }); +}); + +describe("Docker Image Name and Tag Extraction", () => { + describe("extractImageName", () => { + it("should return image name without tag", () => { + expect(extractImageName("my-image:latest")).toBe("my-image"); + expect(extractImageName("my-image:1.0.0")).toBe("my-image"); + expect(extractImageName("ghcr.io/owner/repo:latest")).toBe( + "ghcr.io/owner/repo", + ); + }); + + it("should return full image name when no tag is present", () => { + expect(extractImageName("my-image")).toBe("my-image"); + expect(extractImageName("ghcr.io/owner/repo")).toBe("ghcr.io/owner/repo"); + }); + + it("should handle images with port numbers correctly", () => { + expect(extractImageName("registry:5000/image:tag")).toBe( + "registry:5000/image", + ); + expect(extractImageName("localhost:5000/my-app:latest")).toBe( + "localhost:5000/my-app", + ); + }); + + it("should handle complex image paths", () => { + expect( + extractImageName("myregistryhost:5000/fedora/httpd:version1.0"), + ).toBe("myregistryhost:5000/fedora/httpd"); + expect(extractImageName("registry.example.com:8080/ns/app:v1.2.3")).toBe( + "registry.example.com:8080/ns/app", + ); + }); + + it("should return null for invalid inputs", () => { + expect(extractImageName(null)).toBeNull(); + expect(extractImageName("")).toBeNull(); + }); + + it("should handle edge cases with multiple colons", () => { + expect(extractImageName("image:tag:extra")).toBe("image:tag"); + expect(extractImageName("registry:5000:invalid")).toBe("registry:5000"); + }); + }); + + describe("extractImageTag", () => { + it("should extract tag from image with tag", () => { + expect(extractImageTag("my-image:latest")).toBe("latest"); + expect(extractImageTag("my-image:1.0.0")).toBe("1.0.0"); + expect(extractImageTag("ghcr.io/owner/repo:v1.2.3")).toBe("v1.2.3"); + }); + + it("should return 'latest' when no tag is present", () => { + expect(extractImageTag("my-image")).toBe("latest"); + expect(extractImageTag("ghcr.io/owner/repo")).toBe("latest"); + }); + + it("should handle complex image paths with tags", () => { + expect( + extractImageTag("myregistryhost:5000/fedora/httpd:version1.0"), + ).toBe("version1.0"); + expect(extractImageTag("registry.example.com:8080/ns/app:v1.2.3")).toBe( + "v1.2.3", + ); + }); + + it("should return null for invalid inputs", () => { + expect(extractImageTag(null)).toBeNull(); + expect(extractImageTag("")).toBeNull(); + }); + + it("should handle edge cases with multiple colons", () => { + expect(extractImageTag("image:tag:extra")).toBe("extra"); + expect(extractImageTag("registry:5000/image:tag")).toBe("tag"); + }); + + it("should handle numeric tags", () => { + expect(extractImageTag("my-image:123")).toBe("123"); + expect(extractImageTag("my-image:1")).toBe("1"); + }); + }); +}); diff --git a/apps/dokploy/__test__/env/environment.test.ts b/apps/dokploy/__test__/env/environment.test.ts index 95d46dcc0..24ef18b00 100644 --- a/apps/dokploy/__test__/env/environment.test.ts +++ b/apps/dokploy/__test__/env/environment.test.ts @@ -1,4 +1,7 @@ -import { prepareEnvironmentVariables } from "@dokploy/server/index"; +import { + prepareEnvironmentVariables, + prepareEnvironmentVariablesForShell, +} from "@dokploy/server/index"; import { describe, expect, it } from "vitest"; const projectEnv = ` @@ -332,4 +335,310 @@ IS_DEV=\${{environment.DEVELOPMENT}} "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Γ©"); + }); }); diff --git a/apps/dokploy/components/dashboard/application/deployments/kill-build.tsx b/apps/dokploy/components/dashboard/application/deployments/kill-build.tsx new file mode 100644 index 000000000..784534dd6 --- /dev/null +++ b/apps/dokploy/components/dashboard/application/deployments/kill-build.tsx @@ -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 ( + + + + + + + Are you sure to kill the build? + + This will kill the build process + + + + Cancel + { + await mutateAsync({ + applicationId: id || "", + composeId: id || "", + }) + .then(() => { + toast.success("Build killed successfully"); + }) + .catch((err) => { + toast.error(err.message); + }); + }} + > + Confirm + + + + + ); +}; diff --git a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx index 1045856c2..7f3bc82b4 100644 --- a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx +++ b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx @@ -1,4 +1,12 @@ -import { Clock, Loader2, RefreshCcw, RocketIcon, Settings } from "lucide-react"; +import { + ChevronDown, + ChevronUp, + Clock, + Loader2, + RefreshCcw, + RocketIcon, + Settings, +} from "lucide-react"; import React, { useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import { AlertBlock } from "@/components/shared/alert-block"; @@ -17,6 +25,7 @@ import { import { api, type RouterOutputs } from "@/utils/api"; import { ShowRollbackSettings } from "../rollbacks/show-rollback-settings"; import { CancelQueues } from "./cancel-queues"; +import { KillBuild } from "./kill-build"; import { RefreshToken } from "./refresh-token"; import { ShowDeployment } from "./show-deployment"; @@ -80,6 +89,23 @@ export const ShowDeployments = ({ } = api.compose.cancelDeployment.useMutation(); const [url, setUrl] = React.useState(""); + const [expandedDescriptions, setExpandedDescriptions] = useState>( + new Set(), + ); + + const MAX_DESCRIPTION_LENGTH = 200; + + const truncateDescription = (description: string): string => { + if (description.length <= MAX_DESCRIPTION_LENGTH) { + return description; + } + const truncated = description.slice(0, MAX_DESCRIPTION_LENGTH); + const lastSpace = truncated.lastIndexOf(" "); + if (lastSpace > MAX_DESCRIPTION_LENGTH - 20 && lastSpace > 0) { + return `${truncated.slice(0, lastSpace)}...`; + } + return `${truncated}...`; + }; // Check for stuck deployment (more than 9 minutes) - only for the most recent deployment const stuckDeployment = useMemo(() => { @@ -118,6 +144,9 @@ export const ShowDeployments = ({
+ {(type === "application" || type === "compose") && ( + + )} {(type === "application" || type === "compose") && ( )} @@ -217,118 +246,164 @@ export const ShowDeployments = ({
) : (
- {deployments?.map((deployment, index) => ( -
-
- - {index + 1}. {deployment.status} - - - - {deployment.title} - - {deployment.description && ( - - {deployment.description} + {deployments?.map((deployment, index) => { + const titleText = deployment?.title?.trim() || ""; + const needsTruncation = titleText.length > MAX_DESCRIPTION_LENGTH; + const isExpanded = expandedDescriptions.has( + deployment.deploymentId, + ); + + return ( +
+
+ + {index + 1}. {deployment.status} + - )} -
-
-
- - {deployment.startedAt && deployment.finishedAt && ( - - - {formatDuration( - Math.floor( - (new Date(deployment.finishedAt).getTime() - - new Date(deployment.startedAt).getTime()) / - 1000, - ), - )} - - )} -
-
- {deployment.pid && deployment.status === "running" && ( - { - await killProcess({ - deploymentId: deployment.deploymentId, - }) - .then(() => { - toast.success("Process killed successfully"); - }) - .catch(() => { - toast.error("Error killing process"); - }); - }} - > - - - )} - + {isExpanded ? ( + <> + + Show less + + ) : ( + <> + + Show more + + )} + + )} + {/* Hash (from description) - shown in compact form */} + {deployment.description?.trim() && ( + + {deployment.description} + + )} +
+
+
+
+ + {deployment.startedAt && deployment.finishedAt && ( + + + {formatDuration( + Math.floor( + (new Date(deployment.finishedAt).getTime() - + new Date(deployment.startedAt).getTime()) / + 1000, + ), + )} + + )} +
- {deployment?.rollback && - deployment.status === "done" && - type === "application" && ( +
+ {deployment.pid && deployment.status === "running" && ( { - await rollback({ - rollbackId: deployment.rollback.rollbackId, + await killProcess({ + deploymentId: deployment.deploymentId, }) .then(() => { - toast.success( - "Rollback initiated successfully", - ); + toast.success("Process killed successfully"); }) .catch(() => { - toast.error("Error initiating rollback"); + toast.error("Error killing process"); }); }} > )} + + + {deployment?.rollback && + deployment.status === "done" && + type === "application" && ( + { + await rollback({ + rollbackId: deployment.rollback.rollbackId, + }) + .then(() => { + toast.success( + "Rollback initiated successfully", + ); + }) + .catch(() => { + toast.error("Error initiating rollback"); + }); + }} + > + + + )} +
-
- ))} + ); + })}
)} { id={deployment.previewDeploymentId} type="previewDeployment" serverId={data?.serverId || ""} - /> + > + + Schedule volume backups to run automatically at specified - intervals. + intervals
diff --git a/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx b/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx index 6a0fb030a..01f6944e1 100644 --- a/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx +++ b/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx @@ -1,6 +1,6 @@ import { zodResolver } from "@hookform/resolvers/zod"; import copy from "copy-to-clipboard"; -import { debounce } from "lodash"; +import _ from "lodash"; import { CheckIcon, ChevronsUpDown, @@ -236,7 +236,7 @@ export const RestoreBackup = ({ const currentDatabaseType = form.watch("databaseType"); const metadata = form.watch("metadata"); - const debouncedSetSearch = debounce((value: string) => { + const debouncedSetSearch = _.debounce((value: string) => { setDebouncedSearchTerm(value); }, 350); diff --git a/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx index 5b929f3b6..a75f50386 100644 --- a/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx @@ -1,5 +1,5 @@ import { FancyAnsi } from "fancy-ansi"; -import { escapeRegExp } from "lodash"; +import _ from "lodash"; import { Badge } from "@/components/ui/badge"; import { Tooltip, @@ -47,7 +47,7 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) { } const htmlContent = fancyAnsi.toHtml(text); - const searchRegex = new RegExp(`(${escapeRegExp(term)})`, "gi"); + const searchRegex = new RegExp(`(${_.escapeRegExp(term)})`, "gi"); const modifiedContent = htmlContent.replace( searchRegex, diff --git a/apps/dokploy/components/dashboard/project/add-application.tsx b/apps/dokploy/components/dashboard/project/add-application.tsx index 079701eb8..b0db46681 100644 --- a/apps/dokploy/components/dashboard/project/add-application.tsx +++ b/apps/dokploy/components/dashboard/project/add-application.tsx @@ -150,8 +150,8 @@ export const AddApplication = ({ environmentId, projectName }: Props) => { placeholder="Frontend" {...field} onChange={(e) => { - const val = e.target.value?.trim() || ""; - const serviceName = slugify(val); + const val = e.target.value || ""; + const serviceName = slugify(val.trim()); form.setValue("appName", `${slug}-${serviceName}`); field.onChange(val); }} diff --git a/apps/dokploy/components/dashboard/project/add-compose.tsx b/apps/dokploy/components/dashboard/project/add-compose.tsx index a187104ec..bb911373f 100644 --- a/apps/dokploy/components/dashboard/project/add-compose.tsx +++ b/apps/dokploy/components/dashboard/project/add-compose.tsx @@ -161,8 +161,8 @@ export const AddCompose = ({ environmentId, projectName }: Props) => { placeholder="Frontend" {...field} onChange={(e) => { - const val = e.target.value?.trim() || ""; - const serviceName = slugify(val); + const val = e.target.value || ""; + const serviceName = slugify(val.trim()); form.setValue("appName", `${slug}-${serviceName}`); field.onChange(val); }} diff --git a/apps/dokploy/components/dashboard/project/add-database.tsx b/apps/dokploy/components/dashboard/project/add-database.tsx index c0600a2d9..3176b9589 100644 --- a/apps/dokploy/components/dashboard/project/add-database.tsx +++ b/apps/dokploy/components/dashboard/project/add-database.tsx @@ -395,8 +395,8 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => { placeholder="Name" {...field} onChange={(e) => { - const val = e.target.value?.trim() || ""; - const serviceName = slugify(val); + const val = e.target.value || ""; + const serviceName = slugify(val.trim()); form.setValue("appName", `${slug}-${serviceName}`); field.onChange(val); }} diff --git a/apps/dokploy/components/dashboard/projects/show.tsx b/apps/dokploy/components/dashboard/projects/show.tsx index 388a5d406..5369a544e 100644 --- a/apps/dokploy/components/dashboard/projects/show.tsx +++ b/apps/dokploy/components/dashboard/projects/show.tsx @@ -52,12 +52,14 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { TimeBadge } from "@/components/ui/time-badge"; import { api } from "@/utils/api"; import { HandleProject } from "./handle-project"; import { ProjectEnvironment } from "./project-environment"; export const ShowProjects = () => { const utils = api.useUtils(); + const { data: isCloud } = api.settings.isCloud.useQuery(); const { data, isLoading } = api.project.all.useQuery(); const { data: auth } = api.user.get.useQuery(); const { mutateAsync } = api.project.remove.useMutation(); @@ -135,6 +137,11 @@ export const ShowProjects = () => { + {!isCloud && ( +
+ +
+ )}
@@ -148,7 +155,6 @@ export const ShowProjects = () => { Create and manage your projects - {(auth?.role === "owner" || auth?.canCreateProjects) && (
@@ -298,7 +304,13 @@ export const ShowProjects = () => { {domain.host} @@ -340,7 +352,13 @@ export const ShowProjects = () => { {domain.host} diff --git a/apps/dokploy/components/dashboard/settings/git/show-git-providers.tsx b/apps/dokploy/components/dashboard/settings/git/show-git-providers.tsx index 5f99a2e97..5e4c12696 100644 --- a/apps/dokploy/components/dashboard/settings/git/show-git-providers.tsx +++ b/apps/dokploy/components/dashboard/settings/git/show-git-providers.tsx @@ -48,7 +48,7 @@ export const ShowGitProviders = () => { ) => { const redirectUri = `${url}/api/providers/gitlab/callback?gitlabId=${gitlabId}`; const scope = "api read_user read_repository"; - const authUrl = `${gitlabUrl}/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${encodeURIComponent(scope)}`; + const authUrl = `${gitlabUrl}/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scopes=${encodeURIComponent(scope)}`; return authUrl; }; diff --git a/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx b/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx index 507755bb2..3b5abc8b2 100644 --- a/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx +++ b/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx @@ -1355,8 +1355,10 @@ export const HandleNotifications = ({ notificationId }: Props) => { }); } toast.success("Connection Success"); - } catch { - toast.error("Error testing the provider"); + } catch (error) { + toast.error( + `Error testing the provider ${error instanceof Error ? error.message : "Unknown error"}`, + ); } }} > diff --git a/apps/dokploy/components/dashboard/settings/users/add-permissions.tsx b/apps/dokploy/components/dashboard/settings/users/add-permissions.tsx index fb4d01547..7c6ef8b84 100644 --- a/apps/dokploy/components/dashboard/settings/users/add-permissions.tsx +++ b/apps/dokploy/components/dashboard/settings/users/add-permissions.tsx @@ -1,4 +1,3 @@ -import type { findEnvironmentById } from "@dokploy/server/index"; import { zodResolver } from "@hookform/resolvers/zod"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; @@ -27,12 +26,10 @@ import { FormMessage, } from "@/components/ui/form"; import { Switch } from "@/components/ui/switch"; -import { api } from "@/utils/api"; +import { api, type RouterOutputs } from "@/utils/api"; -type Environment = Omit< - Awaited>, - "project" ->; +type Project = RouterOutputs["project"]["all"][number]; +type Environment = Project["environments"][number]; export type Services = { appName: string; @@ -53,17 +50,16 @@ export type Services = { }; export const extractServices = (data: Environment | undefined) => { - const applications: Services[] = - data?.applications.map((item) => ({ - appName: item.appName, - name: item.name, - type: "application", - id: item.applicationId, - createdAt: item.createdAt, - status: item.applicationStatus, - description: item.description, - serverId: item.serverId, - })) || []; + const applications: Services[] = (data?.applications?.map((item) => ({ + appName: item.appName, + name: item.name, + type: "application", + id: item.applicationId, + createdAt: item.createdAt, + status: item.applicationStatus, + description: item.description, + serverId: item.serverId, + })) ?? []) as Services[]; const mariadb: Services[] = data?.mariadb.map((item) => ({ @@ -125,17 +121,16 @@ export const extractServices = (data: Environment | undefined) => { serverId: item.serverId, })) || []; - const compose: Services[] = - data?.compose.map((item) => ({ - appName: item.appName, - name: item.name, - type: "compose", - id: item.composeId, - createdAt: item.createdAt, - status: item.composeStatus, - description: item.description, - serverId: item.serverId, - })) || []; + const compose: Services[] = (data?.compose?.map((item) => ({ + appName: item.appName, + name: item.name, + type: "compose", + id: item.composeId, + createdAt: item.createdAt, + status: item.composeStatus, + description: item.description, + serverId: item.serverId, + })) ?? []) as Services[]; applications.push( ...mysql, diff --git a/apps/dokploy/components/dashboard/settings/users/show-users.tsx b/apps/dokploy/components/dashboard/settings/users/show-users.tsx index 51d8704a3..06c94416b 100644 --- a/apps/dokploy/components/dashboard/settings/users/show-users.tsx +++ b/apps/dokploy/components/dashboard/settings/users/show-users.tsx @@ -21,7 +21,6 @@ import { import { Table, TableBody, - TableCaption, TableCell, TableHead, TableHeader, @@ -68,7 +67,6 @@ export const ShowUsers = () => { ) : (
- See all users Email @@ -111,35 +109,75 @@ export const ShowUsers = () => { - - - - - - - Actions - + {member.role !== "owner" && ( + + + + + + + Actions + - {member.role !== "owner" && ( - )} - {member.role !== "owner" && ( - <> - {!isCloud && ( - { + {!isCloud && ( + { + await mutateAsync({ + userId: member.user.id, + }) + .then(() => { + toast.success( + "User deleted successfully", + ); + refetch(); + }) + .catch(() => { + toast.error( + "Error deleting destination", + ); + }); + }} + > + e.preventDefault()} + > + Delete User + + + )} + + { + 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, }) @@ -151,86 +189,40 @@ export const ShowUsers = () => { }) .catch(() => { toast.error( - "Error deleting destination", + "Error deleting user", ); }); - }} - > - - e.preventDefault() - } - > - Delete User - - - )} - - { - 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; - } + return; } + } - const { error } = - await authClient.organization.removeMember( - { - memberIdOrEmail: member.id, - }, - ); + const { error } = + await authClient.organization.removeMember( + { + memberIdOrEmail: member.id, + }, + ); - if (!error) { - toast.success( - "User unlinked successfully", - ); - refetch(); - } else { - toast.error( - "Error unlinking user", - ); - } - }} + if (!error) { + toast.success( + "User unlinked successfully", + ); + refetch(); + } else { + toast.error("Error unlinking user"); + } + }} + > + e.preventDefault()} > - e.preventDefault()} - > - Unlink User - - - - )} - - + Unlink User + + + + + )} ); diff --git a/apps/dokploy/components/layouts/side.tsx b/apps/dokploy/components/layouts/side.tsx index 8d84e260c..7473fe586 100644 --- a/apps/dokploy/components/layouts/side.tsx +++ b/apps/dokploy/components/layouts/side.tsx @@ -83,6 +83,7 @@ import { AddOrganization } from "../dashboard/organization/handle-organization"; import { DialogAction } from "../shared/dialog-action"; import { Logo } from "../shared/logo"; import { Button } from "../ui/button"; +import { TimeBadge } from "../ui/time-badge"; import { UpdateServerButton } from "./update-server"; import { UserNav } from "./user-nav"; @@ -1125,6 +1126,7 @@ export default function Page({ children }: Props) { + {!isCloud && } )} diff --git a/apps/dokploy/components/ui/time-badge.tsx b/apps/dokploy/components/ui/time-badge.tsx new file mode 100644 index 000000000..4cf778f25 --- /dev/null +++ b/apps/dokploy/components/ui/time-badge.tsx @@ -0,0 +1,64 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { api } from "@/utils/api"; + +export function TimeBadge() { + const { data: serverTime } = api.server.getServerTime.useQuery(undefined); + const [time, setTime] = useState(null); + + useEffect(() => { + if (serverTime?.time) { + setTime(new Date(serverTime.time)); + } + }, [serverTime]); + + useEffect(() => { + const timer = setInterval(() => { + setTime((prevTime) => { + if (!prevTime) return null; + const newTime = new Date(prevTime.getTime() + 1000); + return newTime; + }); + }, 1000); + + return () => { + clearInterval(timer); + }; + }, []); + + if (!time || !serverTime?.timezone) { + return null; + } + + const getUtcOffset = (timeZone: string) => { + const date = new Date(); + const utcDate = new Date(date.toLocaleString("en-US", { timeZone: "UTC" })); + const tzDate = new Date(date.toLocaleString("en-US", { timeZone })); + const offset = (tzDate.getTime() - utcDate.getTime()) / (1000 * 60 * 60); + const sign = offset >= 0 ? "+" : "-"; + const hours = Math.floor(Math.abs(offset)); + const minutes = (Math.abs(offset) * 60) % 60; + return `UTC${sign}${hours.toString().padStart(2, "0")}:${minutes + .toString() + .padStart(2, "0")}`; + }; + + const formattedTime = new Intl.DateTimeFormat("en-US", { + timeZone: serverTime.timezone, + timeStyle: "medium", + hour12: false, + }).format(time); + + return ( +
+
+ Server Time: + {formattedTime} +
+ + {serverTime.timezone} | {getUtcOffset(serverTime.timezone)} + +
+ ); +} diff --git a/apps/dokploy/drizzle/0121_rainy_cargill.sql b/apps/dokploy/drizzle/0121_rainy_cargill.sql new file mode 100644 index 000000000..85cfa8ceb --- /dev/null +++ b/apps/dokploy/drizzle/0121_rainy_cargill.sql @@ -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%'; + diff --git a/apps/dokploy/drizzle/meta/0121_snapshot.json b/apps/dokploy/drizzle/meta/0121_snapshot.json new file mode 100644 index 000000000..52516ec29 --- /dev/null +++ b/apps/dokploy/drizzle/meta/0121_snapshot.json @@ -0,0 +1,6722 @@ +{ + "id": "6d1361fc-3a46-4016-b6db-42351c20393c", + "prevId": "bbe005b3-d5e0-4e93-8305-ba3b9a2e3f3d", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is2FAEnabled": { + "name": "is2FAEnabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "resetPasswordToken": { + "name": "resetPasswordToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resetPasswordExpiresAt": { + "name": "resetPasswordExpiresAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmationToken": { + "name": "confirmationToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmationExpiresAt": { + "name": "confirmationExpiresAt", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "columnsFrom": [ + "user_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apikey": { + "name": "apikey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "apikey_user_id_user_id_fk": { + "name": "apikey_user_id_user_id_fk", + "tableFrom": "apikey", + "columnsFrom": [ + "user_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "columnsFrom": [ + "organization_id" + ], + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "columnsFrom": [ + "inviter_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canCreateProjects": { + "name": "canCreateProjects", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToSSHKeys": { + "name": "canAccessToSSHKeys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canCreateServices": { + "name": "canCreateServices", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canDeleteProjects": { + "name": "canDeleteProjects", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canDeleteServices": { + "name": "canDeleteServices", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToDocker": { + "name": "canAccessToDocker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToAPI": { + "name": "canAccessToAPI", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToGitProviders": { + "name": "canAccessToGitProviders", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToTraefikFiles": { + "name": "canAccessToTraefikFiles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canDeleteEnvironments": { + "name": "canDeleteEnvironments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canCreateEnvironments": { + "name": "canCreateEnvironments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "accesedProjects": { + "name": "accesedProjects", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "accessedEnvironments": { + "name": "accessedEnvironments", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "accesedServices": { + "name": "accesedServices", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + } + }, + "indexes": {}, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "columnsFrom": [ + "organization_id" + ], + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "columnsFrom": [ + "user_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "organization_owner_id_user_id_fk": { + "name": "organization_owner_id_user_id_fk", + "tableFrom": "organization", + "columnsFrom": [ + "owner_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "columns": [ + "slug" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.two_factor": { + "name": "two_factor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backup_codes": { + "name": "backup_codes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "two_factor_user_id_user_id_fk": { + "name": "two_factor_user_id_user_id_fk", + "tableFrom": "two_factor", + "columnsFrom": [ + "user_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai": { + "name": "ai", + "schema": "", + "columns": { + "aiId": { + "name": "aiId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "apiUrl": { + "name": "apiUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "apiKey": { + "name": "apiKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "isEnabled": { + "name": "isEnabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "ai_organizationId_organization_id_fk": { + "name": "ai_organizationId_organization_id_fk", + "tableFrom": "ai", + "columnsFrom": [ + "organizationId" + ], + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.application": { + "name": "application", + "schema": "", + "columns": { + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewEnv": { + "name": "previewEnv", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "watchPaths": { + "name": "watchPaths", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "previewBuildArgs": { + "name": "previewBuildArgs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewBuildSecrets": { + "name": "previewBuildSecrets", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewLabels": { + "name": "previewLabels", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "previewWildcard": { + "name": "previewWildcard", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewPort": { + "name": "previewPort", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3000 + }, + "previewHttps": { + "name": "previewHttps", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "previewPath": { + "name": "previewPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "previewCustomCertResolver": { + "name": "previewCustomCertResolver", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewLimit": { + "name": "previewLimit", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "isPreviewDeploymentsActive": { + "name": "isPreviewDeploymentsActive", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "previewRequireCollaboratorPermissions": { + "name": "previewRequireCollaboratorPermissions", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rollbackActive": { + "name": "rollbackActive", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "buildArgs": { + "name": "buildArgs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildSecrets": { + "name": "buildSecrets", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "subtitle": { + "name": "subtitle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sourceType": { + "name": "sourceType", + "type": "sourceType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "cleanCache": { + "name": "cleanCache", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "repository": { + "name": "repository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildPath": { + "name": "buildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "triggerType": { + "name": "triggerType", + "type": "triggerType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'push'" + }, + "autoDeploy": { + "name": "autoDeploy", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "gitlabProjectId": { + "name": "gitlabProjectId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "gitlabRepository": { + "name": "gitlabRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabOwner": { + "name": "gitlabOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabBranch": { + "name": "gitlabBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabBuildPath": { + "name": "gitlabBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "gitlabPathNamespace": { + "name": "gitlabPathNamespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaRepository": { + "name": "giteaRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaOwner": { + "name": "giteaOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaBranch": { + "name": "giteaBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaBuildPath": { + "name": "giteaBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "bitbucketRepository": { + "name": "bitbucketRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketOwner": { + "name": "bitbucketOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketBranch": { + "name": "bitbucketBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketBuildPath": { + "name": "bitbucketBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registryUrl": { + "name": "registryUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitUrl": { + "name": "customGitUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitBranch": { + "name": "customGitBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitBuildPath": { + "name": "customGitBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitSSHKeyId": { + "name": "customGitSSHKeyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enableSubmodules": { + "name": "enableSubmodules", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dockerfile": { + "name": "dockerfile", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerContextPath": { + "name": "dockerContextPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerBuildStage": { + "name": "dockerBuildStage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dropBuildPath": { + "name": "dropBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "buildType": { + "name": "buildType", + "type": "buildType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'nixpacks'" + }, + "railpackVersion": { + "name": "railpackVersion", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'0.2.2'" + }, + "herokuVersion": { + "name": "herokuVersion", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'24'" + }, + "publishDirectory": { + "name": "publishDirectory", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isStaticSpa": { + "name": "isStaticSpa", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "registryId": { + "name": "registryId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "githubId": { + "name": "githubId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabId": { + "name": "gitlabId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaId": { + "name": "giteaId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketId": { + "name": "bitbucketId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "application_customGitSSHKeyId_ssh-key_sshKeyId_fk": { + "name": "application_customGitSSHKeyId_ssh-key_sshKeyId_fk", + "tableFrom": "application", + "columnsFrom": [ + "customGitSSHKeyId" + ], + "tableTo": "ssh-key", + "columnsTo": [ + "sshKeyId" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "application_registryId_registry_registryId_fk": { + "name": "application_registryId_registry_registryId_fk", + "tableFrom": "application", + "columnsFrom": [ + "registryId" + ], + "tableTo": "registry", + "columnsTo": [ + "registryId" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "application_environmentId_environment_environmentId_fk": { + "name": "application_environmentId_environment_environmentId_fk", + "tableFrom": "application", + "columnsFrom": [ + "environmentId" + ], + "tableTo": "environment", + "columnsTo": [ + "environmentId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "application_githubId_github_githubId_fk": { + "name": "application_githubId_github_githubId_fk", + "tableFrom": "application", + "columnsFrom": [ + "githubId" + ], + "tableTo": "github", + "columnsTo": [ + "githubId" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "application_gitlabId_gitlab_gitlabId_fk": { + "name": "application_gitlabId_gitlab_gitlabId_fk", + "tableFrom": "application", + "columnsFrom": [ + "gitlabId" + ], + "tableTo": "gitlab", + "columnsTo": [ + "gitlabId" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "application_giteaId_gitea_giteaId_fk": { + "name": "application_giteaId_gitea_giteaId_fk", + "tableFrom": "application", + "columnsFrom": [ + "giteaId" + ], + "tableTo": "gitea", + "columnsTo": [ + "giteaId" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "application_bitbucketId_bitbucket_bitbucketId_fk": { + "name": "application_bitbucketId_bitbucket_bitbucketId_fk", + "tableFrom": "application", + "columnsFrom": [ + "bitbucketId" + ], + "tableTo": "bitbucket", + "columnsTo": [ + "bitbucketId" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "application_serverId_server_serverId_fk": { + "name": "application_serverId_server_serverId_fk", + "tableFrom": "application", + "columnsFrom": [ + "serverId" + ], + "tableTo": "server", + "columnsTo": [ + "serverId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "application_appName_unique": { + "name": "application_appName_unique", + "columns": [ + "appName" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.backup": { + "name": "backup", + "schema": "", + "columns": { + "backupId": { + "name": "backupId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule": { + "name": "schedule", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "database": { + "name": "database", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serviceName": { + "name": "serviceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "destinationId": { + "name": "destinationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "keepLatestCount": { + "name": "keepLatestCount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "backupType": { + "name": "backupType", + "type": "backupType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'database'" + }, + "databaseType": { + "name": "databaseType", + "type": "databaseType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "backup_destinationId_destination_destinationId_fk": { + "name": "backup_destinationId_destination_destinationId_fk", + "tableFrom": "backup", + "columnsFrom": [ + "destinationId" + ], + "tableTo": "destination", + "columnsTo": [ + "destinationId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "backup_composeId_compose_composeId_fk": { + "name": "backup_composeId_compose_composeId_fk", + "tableFrom": "backup", + "columnsFrom": [ + "composeId" + ], + "tableTo": "compose", + "columnsTo": [ + "composeId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "backup_postgresId_postgres_postgresId_fk": { + "name": "backup_postgresId_postgres_postgresId_fk", + "tableFrom": "backup", + "columnsFrom": [ + "postgresId" + ], + "tableTo": "postgres", + "columnsTo": [ + "postgresId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "backup_mariadbId_mariadb_mariadbId_fk": { + "name": "backup_mariadbId_mariadb_mariadbId_fk", + "tableFrom": "backup", + "columnsFrom": [ + "mariadbId" + ], + "tableTo": "mariadb", + "columnsTo": [ + "mariadbId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "backup_mysqlId_mysql_mysqlId_fk": { + "name": "backup_mysqlId_mysql_mysqlId_fk", + "tableFrom": "backup", + "columnsFrom": [ + "mysqlId" + ], + "tableTo": "mysql", + "columnsTo": [ + "mysqlId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "backup_mongoId_mongo_mongoId_fk": { + "name": "backup_mongoId_mongo_mongoId_fk", + "tableFrom": "backup", + "columnsFrom": [ + "mongoId" + ], + "tableTo": "mongo", + "columnsTo": [ + "mongoId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "backup_userId_user_id_fk": { + "name": "backup_userId_user_id_fk", + "tableFrom": "backup", + "columnsFrom": [ + "userId" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "backup_appName_unique": { + "name": "backup_appName_unique", + "columns": [ + "appName" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bitbucket": { + "name": "bitbucket", + "schema": "", + "columns": { + "bitbucketId": { + "name": "bitbucketId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "bitbucketUsername": { + "name": "bitbucketUsername", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketEmail": { + "name": "bitbucketEmail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "appPassword": { + "name": "appPassword", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "apiToken": { + "name": "apiToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketWorkspaceName": { + "name": "bitbucketWorkspaceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "bitbucket_gitProviderId_git_provider_gitProviderId_fk": { + "name": "bitbucket_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "bitbucket", + "columnsFrom": [ + "gitProviderId" + ], + "tableTo": "git_provider", + "columnsTo": [ + "gitProviderId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.certificate": { + "name": "certificate", + "schema": "", + "columns": { + "certificateId": { + "name": "certificateId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "certificateData": { + "name": "certificateData", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "privateKey": { + "name": "privateKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "certificatePath": { + "name": "certificatePath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "autoRenew": { + "name": "autoRenew", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "certificate_organizationId_organization_id_fk": { + "name": "certificate_organizationId_organization_id_fk", + "tableFrom": "certificate", + "columnsFrom": [ + "organizationId" + ], + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "certificate_serverId_server_serverId_fk": { + "name": "certificate_serverId_server_serverId_fk", + "tableFrom": "certificate", + "columnsFrom": [ + "serverId" + ], + "tableTo": "server", + "columnsTo": [ + "serverId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "certificate_certificatePath_unique": { + "name": "certificate_certificatePath_unique", + "columns": [ + "certificatePath" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.compose": { + "name": "compose", + "schema": "", + "columns": { + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeFile": { + "name": "composeFile", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sourceType": { + "name": "sourceType", + "type": "sourceTypeCompose", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "composeType": { + "name": "composeType", + "type": "composeType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'docker-compose'" + }, + "repository": { + "name": "repository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autoDeploy": { + "name": "autoDeploy", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "gitlabProjectId": { + "name": "gitlabProjectId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "gitlabRepository": { + "name": "gitlabRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabOwner": { + "name": "gitlabOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabBranch": { + "name": "gitlabBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabPathNamespace": { + "name": "gitlabPathNamespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketRepository": { + "name": "bitbucketRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketOwner": { + "name": "bitbucketOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketBranch": { + "name": "bitbucketBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaRepository": { + "name": "giteaRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaOwner": { + "name": "giteaOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaBranch": { + "name": "giteaBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitUrl": { + "name": "customGitUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitBranch": { + "name": "customGitBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitSSHKeyId": { + "name": "customGitSSHKeyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "enableSubmodules": { + "name": "enableSubmodules", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "composePath": { + "name": "composePath", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'./docker-compose.yml'" + }, + "suffix": { + "name": "suffix", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "randomize": { + "name": "randomize", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "isolatedDeployment": { + "name": "isolatedDeployment", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "isolatedDeploymentsVolume": { + "name": "isolatedDeploymentsVolume", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "triggerType": { + "name": "triggerType", + "type": "triggerType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'push'" + }, + "composeStatus": { + "name": "composeStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "watchPaths": { + "name": "watchPaths", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "githubId": { + "name": "githubId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabId": { + "name": "gitlabId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketId": { + "name": "bitbucketId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaId": { + "name": "giteaId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "compose_customGitSSHKeyId_ssh-key_sshKeyId_fk": { + "name": "compose_customGitSSHKeyId_ssh-key_sshKeyId_fk", + "tableFrom": "compose", + "columnsFrom": [ + "customGitSSHKeyId" + ], + "tableTo": "ssh-key", + "columnsTo": [ + "sshKeyId" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "compose_environmentId_environment_environmentId_fk": { + "name": "compose_environmentId_environment_environmentId_fk", + "tableFrom": "compose", + "columnsFrom": [ + "environmentId" + ], + "tableTo": "environment", + "columnsTo": [ + "environmentId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "compose_githubId_github_githubId_fk": { + "name": "compose_githubId_github_githubId_fk", + "tableFrom": "compose", + "columnsFrom": [ + "githubId" + ], + "tableTo": "github", + "columnsTo": [ + "githubId" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "compose_gitlabId_gitlab_gitlabId_fk": { + "name": "compose_gitlabId_gitlab_gitlabId_fk", + "tableFrom": "compose", + "columnsFrom": [ + "gitlabId" + ], + "tableTo": "gitlab", + "columnsTo": [ + "gitlabId" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "compose_bitbucketId_bitbucket_bitbucketId_fk": { + "name": "compose_bitbucketId_bitbucket_bitbucketId_fk", + "tableFrom": "compose", + "columnsFrom": [ + "bitbucketId" + ], + "tableTo": "bitbucket", + "columnsTo": [ + "bitbucketId" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "compose_giteaId_gitea_giteaId_fk": { + "name": "compose_giteaId_gitea_giteaId_fk", + "tableFrom": "compose", + "columnsFrom": [ + "giteaId" + ], + "tableTo": "gitea", + "columnsTo": [ + "giteaId" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "compose_serverId_server_serverId_fk": { + "name": "compose_serverId_server_serverId_fk", + "tableFrom": "compose", + "columnsFrom": [ + "serverId" + ], + "tableTo": "server", + "columnsTo": [ + "serverId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment": { + "name": "deployment", + "schema": "", + "columns": { + "deploymentId": { + "name": "deploymentId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "deploymentStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'running'" + }, + "logPath": { + "name": "logPath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pid": { + "name": "pid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isPreviewDeployment": { + "name": "isPreviewDeployment", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "previewDeploymentId": { + "name": "previewDeploymentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "startedAt": { + "name": "startedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finishedAt": { + "name": "finishedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "errorMessage": { + "name": "errorMessage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduleId": { + "name": "scheduleId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backupId": { + "name": "backupId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rollbackId": { + "name": "rollbackId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "volumeBackupId": { + "name": "volumeBackupId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "deployment_applicationId_application_applicationId_fk": { + "name": "deployment_applicationId_application_applicationId_fk", + "tableFrom": "deployment", + "columnsFrom": [ + "applicationId" + ], + "tableTo": "application", + "columnsTo": [ + "applicationId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "deployment_composeId_compose_composeId_fk": { + "name": "deployment_composeId_compose_composeId_fk", + "tableFrom": "deployment", + "columnsFrom": [ + "composeId" + ], + "tableTo": "compose", + "columnsTo": [ + "composeId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "deployment_serverId_server_serverId_fk": { + "name": "deployment_serverId_server_serverId_fk", + "tableFrom": "deployment", + "columnsFrom": [ + "serverId" + ], + "tableTo": "server", + "columnsTo": [ + "serverId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "deployment_previewDeploymentId_preview_deployments_previewDeploymentId_fk": { + "name": "deployment_previewDeploymentId_preview_deployments_previewDeploymentId_fk", + "tableFrom": "deployment", + "columnsFrom": [ + "previewDeploymentId" + ], + "tableTo": "preview_deployments", + "columnsTo": [ + "previewDeploymentId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "deployment_scheduleId_schedule_scheduleId_fk": { + "name": "deployment_scheduleId_schedule_scheduleId_fk", + "tableFrom": "deployment", + "columnsFrom": [ + "scheduleId" + ], + "tableTo": "schedule", + "columnsTo": [ + "scheduleId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "deployment_backupId_backup_backupId_fk": { + "name": "deployment_backupId_backup_backupId_fk", + "tableFrom": "deployment", + "columnsFrom": [ + "backupId" + ], + "tableTo": "backup", + "columnsTo": [ + "backupId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "deployment_rollbackId_rollback_rollbackId_fk": { + "name": "deployment_rollbackId_rollback_rollbackId_fk", + "tableFrom": "deployment", + "columnsFrom": [ + "rollbackId" + ], + "tableTo": "rollback", + "columnsTo": [ + "rollbackId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "deployment_volumeBackupId_volume_backup_volumeBackupId_fk": { + "name": "deployment_volumeBackupId_volume_backup_volumeBackupId_fk", + "tableFrom": "deployment", + "columnsFrom": [ + "volumeBackupId" + ], + "tableTo": "volume_backup", + "columnsTo": [ + "volumeBackupId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.destination": { + "name": "destination", + "schema": "", + "columns": { + "destinationId": { + "name": "destinationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessKey": { + "name": "accessKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secretAccessKey": { + "name": "secretAccessKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bucket": { + "name": "bucket", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "destination_organizationId_organization_id_fk": { + "name": "destination_organizationId_organization_id_fk", + "tableFrom": "destination", + "columnsFrom": [ + "organizationId" + ], + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.domain": { + "name": "domain", + "schema": "", + "columns": { + "domainId": { + "name": "domainId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "https": { + "name": "https", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3000 + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "serviceName": { + "name": "serviceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domainType": { + "name": "domainType", + "type": "domainType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'application'" + }, + "uniqueConfigKey": { + "name": "uniqueConfigKey", + "type": "serial", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customCertResolver": { + "name": "customCertResolver", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewDeploymentId": { + "name": "previewDeploymentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "internalPath": { + "name": "internalPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "stripPath": { + "name": "stripPath", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "domain_composeId_compose_composeId_fk": { + "name": "domain_composeId_compose_composeId_fk", + "tableFrom": "domain", + "columnsFrom": [ + "composeId" + ], + "tableTo": "compose", + "columnsTo": [ + "composeId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "domain_applicationId_application_applicationId_fk": { + "name": "domain_applicationId_application_applicationId_fk", + "tableFrom": "domain", + "columnsFrom": [ + "applicationId" + ], + "tableTo": "application", + "columnsTo": [ + "applicationId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "domain_previewDeploymentId_preview_deployments_previewDeploymentId_fk": { + "name": "domain_previewDeploymentId_preview_deployments_previewDeploymentId_fk", + "tableFrom": "domain", + "columnsFrom": [ + "previewDeploymentId" + ], + "tableTo": "preview_deployments", + "columnsTo": [ + "previewDeploymentId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "environment_projectId_project_projectId_fk": { + "name": "environment_projectId_project_projectId_fk", + "tableFrom": "environment", + "columnsFrom": [ + "projectId" + ], + "tableTo": "project", + "columnsTo": [ + "projectId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_provider": { + "name": "git_provider", + "schema": "", + "columns": { + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerType": { + "name": "providerType", + "type": "gitProviderType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "git_provider_organizationId_organization_id_fk": { + "name": "git_provider_organizationId_organization_id_fk", + "tableFrom": "git_provider", + "columnsFrom": [ + "organizationId" + ], + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "git_provider_userId_user_id_fk": { + "name": "git_provider_userId_user_id_fk", + "tableFrom": "git_provider", + "columnsFrom": [ + "userId" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.gitea": { + "name": "gitea", + "schema": "", + "columns": { + "giteaId": { + "name": "giteaId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "giteaUrl": { + "name": "giteaUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'https://gitea.com'" + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'repo,repo:status,read:user,read:org'" + }, + "last_authenticated_at": { + "name": "last_authenticated_at", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "gitea_gitProviderId_git_provider_gitProviderId_fk": { + "name": "gitea_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "gitea", + "columnsFrom": [ + "gitProviderId" + ], + "tableTo": "git_provider", + "columnsTo": [ + "gitProviderId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.github": { + "name": "github", + "schema": "", + "columns": { + "githubId": { + "name": "githubId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "githubAppName": { + "name": "githubAppName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubAppId": { + "name": "githubAppId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "githubClientId": { + "name": "githubClientId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubClientSecret": { + "name": "githubClientSecret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubInstallationId": { + "name": "githubInstallationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubPrivateKey": { + "name": "githubPrivateKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubWebhookSecret": { + "name": "githubWebhookSecret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "github_gitProviderId_git_provider_gitProviderId_fk": { + "name": "github_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "github", + "columnsFrom": [ + "gitProviderId" + ], + "tableTo": "git_provider", + "columnsTo": [ + "gitProviderId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.gitlab": { + "name": "gitlab", + "schema": "", + "columns": { + "gitlabId": { + "name": "gitlabId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "gitlabUrl": { + "name": "gitlabUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'https://gitlab.com'" + }, + "application_id": { + "name": "application_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "group_name": { + "name": "group_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "gitlab_gitProviderId_git_provider_gitProviderId_fk": { + "name": "gitlab_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "gitlab", + "columnsFrom": [ + "gitProviderId" + ], + "tableTo": "git_provider", + "columnsTo": [ + "gitProviderId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mariadb": { + "name": "mariadb", + "schema": "", + "columns": { + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseName": { + "name": "databaseName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rootPassword": { + "name": "rootPassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "mariadb_environmentId_environment_environmentId_fk": { + "name": "mariadb_environmentId_environment_environmentId_fk", + "tableFrom": "mariadb", + "columnsFrom": [ + "environmentId" + ], + "tableTo": "environment", + "columnsTo": [ + "environmentId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "mariadb_serverId_server_serverId_fk": { + "name": "mariadb_serverId_server_serverId_fk", + "tableFrom": "mariadb", + "columnsFrom": [ + "serverId" + ], + "tableTo": "server", + "columnsTo": [ + "serverId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mariadb_appName_unique": { + "name": "mariadb_appName_unique", + "columns": [ + "appName" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mongo": { + "name": "mongo", + "schema": "", + "columns": { + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "replicaSets": { + "name": "replicaSets", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "mongo_environmentId_environment_environmentId_fk": { + "name": "mongo_environmentId_environment_environmentId_fk", + "tableFrom": "mongo", + "columnsFrom": [ + "environmentId" + ], + "tableTo": "environment", + "columnsTo": [ + "environmentId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "mongo_serverId_server_serverId_fk": { + "name": "mongo_serverId_server_serverId_fk", + "tableFrom": "mongo", + "columnsFrom": [ + "serverId" + ], + "tableTo": "server", + "columnsTo": [ + "serverId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mongo_appName_unique": { + "name": "mongo_appName_unique", + "columns": [ + "appName" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mount": { + "name": "mount", + "schema": "", + "columns": { + "mountId": { + "name": "mountId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "mountType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "hostPath": { + "name": "hostPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "volumeName": { + "name": "volumeName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "filePath": { + "name": "filePath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serviceType": { + "name": "serviceType", + "type": "serviceType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'application'" + }, + "mountPath": { + "name": "mountPath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redisId": { + "name": "redisId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "mount_applicationId_application_applicationId_fk": { + "name": "mount_applicationId_application_applicationId_fk", + "tableFrom": "mount", + "columnsFrom": [ + "applicationId" + ], + "tableTo": "application", + "columnsTo": [ + "applicationId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "mount_postgresId_postgres_postgresId_fk": { + "name": "mount_postgresId_postgres_postgresId_fk", + "tableFrom": "mount", + "columnsFrom": [ + "postgresId" + ], + "tableTo": "postgres", + "columnsTo": [ + "postgresId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "mount_mariadbId_mariadb_mariadbId_fk": { + "name": "mount_mariadbId_mariadb_mariadbId_fk", + "tableFrom": "mount", + "columnsFrom": [ + "mariadbId" + ], + "tableTo": "mariadb", + "columnsTo": [ + "mariadbId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "mount_mongoId_mongo_mongoId_fk": { + "name": "mount_mongoId_mongo_mongoId_fk", + "tableFrom": "mount", + "columnsFrom": [ + "mongoId" + ], + "tableTo": "mongo", + "columnsTo": [ + "mongoId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "mount_mysqlId_mysql_mysqlId_fk": { + "name": "mount_mysqlId_mysql_mysqlId_fk", + "tableFrom": "mount", + "columnsFrom": [ + "mysqlId" + ], + "tableTo": "mysql", + "columnsTo": [ + "mysqlId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "mount_redisId_redis_redisId_fk": { + "name": "mount_redisId_redis_redisId_fk", + "tableFrom": "mount", + "columnsFrom": [ + "redisId" + ], + "tableTo": "redis", + "columnsTo": [ + "redisId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "mount_composeId_compose_composeId_fk": { + "name": "mount_composeId_compose_composeId_fk", + "tableFrom": "mount", + "columnsFrom": [ + "composeId" + ], + "tableTo": "compose", + "columnsTo": [ + "composeId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mysql": { + "name": "mysql", + "schema": "", + "columns": { + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseName": { + "name": "databaseName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rootPassword": { + "name": "rootPassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "mysql_environmentId_environment_environmentId_fk": { + "name": "mysql_environmentId_environment_environmentId_fk", + "tableFrom": "mysql", + "columnsFrom": [ + "environmentId" + ], + "tableTo": "environment", + "columnsTo": [ + "environmentId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "mysql_serverId_server_serverId_fk": { + "name": "mysql_serverId_server_serverId_fk", + "tableFrom": "mysql", + "columnsFrom": [ + "serverId" + ], + "tableTo": "server", + "columnsTo": [ + "serverId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mysql_appName_unique": { + "name": "mysql_appName_unique", + "columns": [ + "appName" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.discord": { + "name": "discord", + "schema": "", + "columns": { + "discordId": { + "name": "discordId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "decoration": { + "name": "decoration", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email": { + "name": "email", + "schema": "", + "columns": { + "emailId": { + "name": "emailId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "smtpServer": { + "name": "smtpServer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "smtpPort": { + "name": "smtpPort", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fromAddress": { + "name": "fromAddress", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "toAddress": { + "name": "toAddress", + "type": "text[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.gotify": { + "name": "gotify", + "schema": "", + "columns": { + "gotifyId": { + "name": "gotifyId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "serverUrl": { + "name": "serverUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appToken": { + "name": "appToken", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "decoration": { + "name": "decoration", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.lark": { + "name": "lark", + "schema": "", + "columns": { + "larkId": { + "name": "larkId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification": { + "name": "notification", + "schema": "", + "columns": { + "notificationId": { + "name": "notificationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appDeploy": { + "name": "appDeploy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "appBuildError": { + "name": "appBuildError", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "databaseBackup": { + "name": "databaseBackup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dokployRestart": { + "name": "dokployRestart", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dockerCleanup": { + "name": "dockerCleanup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "serverThreshold": { + "name": "serverThreshold", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "notificationType": { + "name": "notificationType", + "type": "notificationType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slackId": { + "name": "slackId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telegramId": { + "name": "telegramId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "discordId": { + "name": "discordId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "emailId": { + "name": "emailId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gotifyId": { + "name": "gotifyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ntfyId": { + "name": "ntfyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "larkId": { + "name": "larkId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "notification_slackId_slack_slackId_fk": { + "name": "notification_slackId_slack_slackId_fk", + "tableFrom": "notification", + "columnsFrom": [ + "slackId" + ], + "tableTo": "slack", + "columnsTo": [ + "slackId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "notification_telegramId_telegram_telegramId_fk": { + "name": "notification_telegramId_telegram_telegramId_fk", + "tableFrom": "notification", + "columnsFrom": [ + "telegramId" + ], + "tableTo": "telegram", + "columnsTo": [ + "telegramId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "notification_discordId_discord_discordId_fk": { + "name": "notification_discordId_discord_discordId_fk", + "tableFrom": "notification", + "columnsFrom": [ + "discordId" + ], + "tableTo": "discord", + "columnsTo": [ + "discordId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "notification_emailId_email_emailId_fk": { + "name": "notification_emailId_email_emailId_fk", + "tableFrom": "notification", + "columnsFrom": [ + "emailId" + ], + "tableTo": "email", + "columnsTo": [ + "emailId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "notification_gotifyId_gotify_gotifyId_fk": { + "name": "notification_gotifyId_gotify_gotifyId_fk", + "tableFrom": "notification", + "columnsFrom": [ + "gotifyId" + ], + "tableTo": "gotify", + "columnsTo": [ + "gotifyId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "notification_ntfyId_ntfy_ntfyId_fk": { + "name": "notification_ntfyId_ntfy_ntfyId_fk", + "tableFrom": "notification", + "columnsFrom": [ + "ntfyId" + ], + "tableTo": "ntfy", + "columnsTo": [ + "ntfyId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "notification_larkId_lark_larkId_fk": { + "name": "notification_larkId_lark_larkId_fk", + "tableFrom": "notification", + "columnsFrom": [ + "larkId" + ], + "tableTo": "lark", + "columnsTo": [ + "larkId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "notification_organizationId_organization_id_fk": { + "name": "notification_organizationId_organization_id_fk", + "tableFrom": "notification", + "columnsFrom": [ + "organizationId" + ], + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ntfy": { + "name": "ntfy", + "schema": "", + "columns": { + "ntfyId": { + "name": "ntfyId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "serverUrl": { + "name": "serverUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "topic": { + "name": "topic", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "accessToken": { + "name": "accessToken", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.slack": { + "name": "slack", + "schema": "", + "columns": { + "slackId": { + "name": "slackId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.telegram": { + "name": "telegram", + "schema": "", + "columns": { + "telegramId": { + "name": "telegramId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "botToken": { + "name": "botToken", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chatId": { + "name": "chatId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "messageThreadId": { + "name": "messageThreadId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.port": { + "name": "port", + "schema": "", + "columns": { + "portId": { + "name": "portId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "publishedPort": { + "name": "publishedPort", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "publishMode": { + "name": "publishMode", + "type": "publishModeType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'host'" + }, + "targetPort": { + "name": "targetPort", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "protocolType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "port_applicationId_application_applicationId_fk": { + "name": "port_applicationId_application_applicationId_fk", + "tableFrom": "port", + "columnsFrom": [ + "applicationId" + ], + "tableTo": "application", + "columnsTo": [ + "applicationId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.postgres": { + "name": "postgres", + "schema": "", + "columns": { + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseName": { + "name": "databaseName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "postgres_environmentId_environment_environmentId_fk": { + "name": "postgres_environmentId_environment_environmentId_fk", + "tableFrom": "postgres", + "columnsFrom": [ + "environmentId" + ], + "tableTo": "environment", + "columnsTo": [ + "environmentId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "postgres_serverId_server_serverId_fk": { + "name": "postgres_serverId_server_serverId_fk", + "tableFrom": "postgres", + "columnsFrom": [ + "serverId" + ], + "tableTo": "server", + "columnsTo": [ + "serverId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "postgres_appName_unique": { + "name": "postgres_appName_unique", + "columns": [ + "appName" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.preview_deployments": { + "name": "preview_deployments", + "schema": "", + "columns": { + "previewDeploymentId": { + "name": "previewDeploymentId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestId": { + "name": "pullRequestId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestNumber": { + "name": "pullRequestNumber", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestURL": { + "name": "pullRequestURL", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestTitle": { + "name": "pullRequestTitle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestCommentId": { + "name": "pullRequestCommentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "previewStatus": { + "name": "previewStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domainId": { + "name": "domainId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "preview_deployments_applicationId_application_applicationId_fk": { + "name": "preview_deployments_applicationId_application_applicationId_fk", + "tableFrom": "preview_deployments", + "columnsFrom": [ + "applicationId" + ], + "tableTo": "application", + "columnsTo": [ + "applicationId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "preview_deployments_domainId_domain_domainId_fk": { + "name": "preview_deployments_domainId_domain_domainId_fk", + "tableFrom": "preview_deployments", + "columnsFrom": [ + "domainId" + ], + "tableTo": "domain", + "columnsTo": [ + "domainId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "preview_deployments_appName_unique": { + "name": "preview_deployments_appName_unique", + "columns": [ + "appName" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project": { + "name": "project", + "schema": "", + "columns": { + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": { + "project_organizationId_organization_id_fk": { + "name": "project_organizationId_organization_id_fk", + "tableFrom": "project", + "columnsFrom": [ + "organizationId" + ], + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.redirect": { + "name": "redirect", + "schema": "", + "columns": { + "redirectId": { + "name": "redirectId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "regex": { + "name": "regex", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "replacement": { + "name": "replacement", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permanent": { + "name": "permanent", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "uniqueConfigKey": { + "name": "uniqueConfigKey", + "type": "serial", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "redirect_applicationId_application_applicationId_fk": { + "name": "redirect_applicationId_application_applicationId_fk", + "tableFrom": "redirect", + "columnsFrom": [ + "applicationId" + ], + "tableTo": "application", + "columnsTo": [ + "applicationId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.redis": { + "name": "redis", + "schema": "", + "columns": { + "redisId": { + "name": "redisId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "redis_environmentId_environment_environmentId_fk": { + "name": "redis_environmentId_environment_environmentId_fk", + "tableFrom": "redis", + "columnsFrom": [ + "environmentId" + ], + "tableTo": "environment", + "columnsTo": [ + "environmentId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "redis_serverId_server_serverId_fk": { + "name": "redis_serverId_server_serverId_fk", + "tableFrom": "redis", + "columnsFrom": [ + "serverId" + ], + "tableTo": "server", + "columnsTo": [ + "serverId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "redis_appName_unique": { + "name": "redis_appName_unique", + "columns": [ + "appName" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.registry": { + "name": "registry", + "schema": "", + "columns": { + "registryId": { + "name": "registryId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "registryName": { + "name": "registryName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "imagePrefix": { + "name": "imagePrefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "registryUrl": { + "name": "registryUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "selfHosted": { + "name": "selfHosted", + "type": "RegistryType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'cloud'" + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "registry_organizationId_organization_id_fk": { + "name": "registry_organizationId_organization_id_fk", + "tableFrom": "registry", + "columnsFrom": [ + "organizationId" + ], + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rollback": { + "name": "rollback", + "schema": "", + "columns": { + "rollbackId": { + "name": "rollbackId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "deploymentId": { + "name": "deploymentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "serial", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fullContext": { + "name": "fullContext", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "rollback_deploymentId_deployment_deploymentId_fk": { + "name": "rollback_deploymentId_deployment_deploymentId_fk", + "tableFrom": "rollback", + "columnsFrom": [ + "deploymentId" + ], + "tableTo": "deployment", + "columnsTo": [ + "deploymentId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.schedule": { + "name": "schedule", + "schema": "", + "columns": { + "scheduleId": { + "name": "scheduleId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cronExpression": { + "name": "cronExpression", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serviceName": { + "name": "serviceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shellType": { + "name": "shellType", + "type": "shellType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'bash'" + }, + "scheduleType": { + "name": "scheduleType", + "type": "scheduleType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'application'" + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "script": { + "name": "script", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "schedule_applicationId_application_applicationId_fk": { + "name": "schedule_applicationId_application_applicationId_fk", + "tableFrom": "schedule", + "columnsFrom": [ + "applicationId" + ], + "tableTo": "application", + "columnsTo": [ + "applicationId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "schedule_composeId_compose_composeId_fk": { + "name": "schedule_composeId_compose_composeId_fk", + "tableFrom": "schedule", + "columnsFrom": [ + "composeId" + ], + "tableTo": "compose", + "columnsTo": [ + "composeId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "schedule_serverId_server_serverId_fk": { + "name": "schedule_serverId_server_serverId_fk", + "tableFrom": "schedule", + "columnsFrom": [ + "serverId" + ], + "tableTo": "server", + "columnsTo": [ + "serverId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "schedule_userId_user_id_fk": { + "name": "schedule_userId_user_id_fk", + "tableFrom": "schedule", + "columnsFrom": [ + "userId" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.security": { + "name": "security", + "schema": "", + "columns": { + "securityId": { + "name": "securityId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "security_applicationId_application_applicationId_fk": { + "name": "security_applicationId_application_applicationId_fk", + "tableFrom": "security", + "columnsFrom": [ + "applicationId" + ], + "tableTo": "application", + "columnsTo": [ + "applicationId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "security_username_applicationId_unique": { + "name": "security_username_applicationId_unique", + "columns": [ + "username", + "applicationId" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.server": { + "name": "server", + "schema": "", + "columns": { + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ipAddress": { + "name": "ipAddress", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'root'" + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enableDockerCleanup": { + "name": "enableDockerCleanup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverStatus": { + "name": "serverStatus", + "type": "serverStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "sshKeyId": { + "name": "sshKeyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metricsConfig": { + "name": "metricsConfig", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"server\":{\"type\":\"Remote\",\"refreshRate\":60,\"port\":4500,\"token\":\"\",\"urlCallback\":\"\",\"cronJob\":\"\",\"retentionDays\":2,\"thresholds\":{\"cpu\":0,\"memory\":0}},\"containers\":{\"refreshRate\":60,\"services\":{\"include\":[],\"exclude\":[]}}}'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "server_organizationId_organization_id_fk": { + "name": "server_organizationId_organization_id_fk", + "tableFrom": "server", + "columnsFrom": [ + "organizationId" + ], + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "server_sshKeyId_ssh-key_sshKeyId_fk": { + "name": "server_sshKeyId_ssh-key_sshKeyId_fk", + "tableFrom": "server", + "columnsFrom": [ + "sshKeyId" + ], + "tableTo": "ssh-key", + "columnsTo": [ + "sshKeyId" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_temp": { + "name": "session_temp", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_temp_user_id_user_id_fk": { + "name": "session_temp_user_id_user_id_fk", + "tableFrom": "session_temp", + "columnsFrom": [ + "user_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_temp_token_unique": { + "name": "session_temp_token_unique", + "columns": [ + "token" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh-key": { + "name": "ssh-key", + "schema": "", + "columns": { + "sshKeyId": { + "name": "sshKeyId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "privateKey": { + "name": "privateKey", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "publicKey": { + "name": "publicKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lastUsedAt": { + "name": "lastUsedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "ssh-key_organizationId_organization_id_fk": { + "name": "ssh-key_organizationId_organization_id_fk", + "tableFrom": "ssh-key", + "columnsFrom": [ + "organizationId" + ], + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "isRegistered": { + "name": "isRegistered", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "expirationDate": { + "name": "expirationDate", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "two_factor_enabled": { + "name": "two_factor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "serverIp": { + "name": "serverIp", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "https": { + "name": "https", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "letsEncryptEmail": { + "name": "letsEncryptEmail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sshPrivateKey": { + "name": "sshPrivateKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enableDockerCleanup": { + "name": "enableDockerCleanup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "logCleanupCron": { + "name": "logCleanupCron", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'0 0 * * *'" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "enablePaidFeatures": { + "name": "enablePaidFeatures", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "allowImpersonation": { + "name": "allowImpersonation", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "metricsConfig": { + "name": "metricsConfig", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"server\":{\"type\":\"Dokploy\",\"refreshRate\":60,\"port\":4500,\"token\":\"\",\"retentionDays\":2,\"cronJob\":\"\",\"urlCallback\":\"\",\"thresholds\":{\"cpu\":0,\"memory\":0}},\"containers\":{\"refreshRate\":60,\"services\":{\"include\":[],\"exclude\":[]}}}'::jsonb" + }, + "cleanupCacheApplications": { + "name": "cleanupCacheApplications", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cleanupCacheOnPreviews": { + "name": "cleanupCacheOnPreviews", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cleanupCacheOnCompose": { + "name": "cleanupCacheOnCompose", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "stripeCustomerId": { + "name": "stripeCustomerId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripeSubscriptionId": { + "name": "stripeSubscriptionId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serversQuantity": { + "name": "serversQuantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.volume_backup": { + "name": "volume_backup", + "schema": "", + "columns": { + "volumeBackupId": { + "name": "volumeBackupId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "volumeName": { + "name": "volumeName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serviceType": { + "name": "serviceType", + "type": "serviceType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'application'" + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serviceName": { + "name": "serviceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "turnOff": { + "name": "turnOff", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cronExpression": { + "name": "cronExpression", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "keepLatestCount": { + "name": "keepLatestCount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redisId": { + "name": "redisId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "destinationId": { + "name": "destinationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "volume_backup_applicationId_application_applicationId_fk": { + "name": "volume_backup_applicationId_application_applicationId_fk", + "tableFrom": "volume_backup", + "columnsFrom": [ + "applicationId" + ], + "tableTo": "application", + "columnsTo": [ + "applicationId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "volume_backup_postgresId_postgres_postgresId_fk": { + "name": "volume_backup_postgresId_postgres_postgresId_fk", + "tableFrom": "volume_backup", + "columnsFrom": [ + "postgresId" + ], + "tableTo": "postgres", + "columnsTo": [ + "postgresId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "volume_backup_mariadbId_mariadb_mariadbId_fk": { + "name": "volume_backup_mariadbId_mariadb_mariadbId_fk", + "tableFrom": "volume_backup", + "columnsFrom": [ + "mariadbId" + ], + "tableTo": "mariadb", + "columnsTo": [ + "mariadbId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "volume_backup_mongoId_mongo_mongoId_fk": { + "name": "volume_backup_mongoId_mongo_mongoId_fk", + "tableFrom": "volume_backup", + "columnsFrom": [ + "mongoId" + ], + "tableTo": "mongo", + "columnsTo": [ + "mongoId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "volume_backup_mysqlId_mysql_mysqlId_fk": { + "name": "volume_backup_mysqlId_mysql_mysqlId_fk", + "tableFrom": "volume_backup", + "columnsFrom": [ + "mysqlId" + ], + "tableTo": "mysql", + "columnsTo": [ + "mysqlId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "volume_backup_redisId_redis_redisId_fk": { + "name": "volume_backup_redisId_redis_redisId_fk", + "tableFrom": "volume_backup", + "columnsFrom": [ + "redisId" + ], + "tableTo": "redis", + "columnsTo": [ + "redisId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "volume_backup_composeId_compose_composeId_fk": { + "name": "volume_backup_composeId_compose_composeId_fk", + "tableFrom": "volume_backup", + "columnsFrom": [ + "composeId" + ], + "tableTo": "compose", + "columnsTo": [ + "composeId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "volume_backup_destinationId_destination_destinationId_fk": { + "name": "volume_backup_destinationId_destination_destinationId_fk", + "tableFrom": "volume_backup", + "columnsFrom": [ + "destinationId" + ], + "tableTo": "destination", + "columnsTo": [ + "destinationId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.buildType": { + "name": "buildType", + "schema": "public", + "values": [ + "dockerfile", + "heroku_buildpacks", + "paketo_buildpacks", + "nixpacks", + "static", + "railpack" + ] + }, + "public.sourceType": { + "name": "sourceType", + "schema": "public", + "values": [ + "docker", + "git", + "github", + "gitlab", + "bitbucket", + "gitea", + "drop" + ] + }, + "public.backupType": { + "name": "backupType", + "schema": "public", + "values": [ + "database", + "compose" + ] + }, + "public.databaseType": { + "name": "databaseType", + "schema": "public", + "values": [ + "postgres", + "mariadb", + "mysql", + "mongo", + "web-server" + ] + }, + "public.composeType": { + "name": "composeType", + "schema": "public", + "values": [ + "docker-compose", + "stack" + ] + }, + "public.sourceTypeCompose": { + "name": "sourceTypeCompose", + "schema": "public", + "values": [ + "git", + "github", + "gitlab", + "bitbucket", + "gitea", + "raw" + ] + }, + "public.deploymentStatus": { + "name": "deploymentStatus", + "schema": "public", + "values": [ + "running", + "done", + "error", + "cancelled" + ] + }, + "public.domainType": { + "name": "domainType", + "schema": "public", + "values": [ + "compose", + "application", + "preview" + ] + }, + "public.gitProviderType": { + "name": "gitProviderType", + "schema": "public", + "values": [ + "github", + "gitlab", + "bitbucket", + "gitea" + ] + }, + "public.mountType": { + "name": "mountType", + "schema": "public", + "values": [ + "bind", + "volume", + "file" + ] + }, + "public.serviceType": { + "name": "serviceType", + "schema": "public", + "values": [ + "application", + "postgres", + "mysql", + "mariadb", + "mongo", + "redis", + "compose" + ] + }, + "public.notificationType": { + "name": "notificationType", + "schema": "public", + "values": [ + "slack", + "telegram", + "discord", + "email", + "gotify", + "ntfy", + "lark" + ] + }, + "public.protocolType": { + "name": "protocolType", + "schema": "public", + "values": [ + "tcp", + "udp" + ] + }, + "public.publishModeType": { + "name": "publishModeType", + "schema": "public", + "values": [ + "ingress", + "host" + ] + }, + "public.RegistryType": { + "name": "RegistryType", + "schema": "public", + "values": [ + "selfHosted", + "cloud" + ] + }, + "public.scheduleType": { + "name": "scheduleType", + "schema": "public", + "values": [ + "application", + "compose", + "server", + "dokploy-server" + ] + }, + "public.shellType": { + "name": "shellType", + "schema": "public", + "values": [ + "bash", + "sh" + ] + }, + "public.serverStatus": { + "name": "serverStatus", + "schema": "public", + "values": [ + "active", + "inactive" + ] + }, + "public.applicationStatus": { + "name": "applicationStatus", + "schema": "public", + "values": [ + "idle", + "running", + "done", + "error" + ] + }, + "public.certificateType": { + "name": "certificateType", + "schema": "public", + "values": [ + "letsencrypt", + "none", + "custom" + ] + }, + "public.triggerType": { + "name": "triggerType", + "schema": "public", + "values": [ + "push", + "tag" + ] + } + }, + "schemas": {}, + "views": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/dokploy/drizzle/meta/_journal.json b/apps/dokploy/drizzle/meta/_journal.json index c51c28816..323562fd1 100644 --- a/apps/dokploy/drizzle/meta/_journal.json +++ b/apps/dokploy/drizzle/meta/_journal.json @@ -848,6 +848,13 @@ "when": 1762632540024, "tag": "0120_lame_captain_midlands", "breakpoints": true + }, + { + "idx": 121, + "version": "7", + "when": 1763755037033, + "tag": "0121_rainy_cargill", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index 183771f79..1301fe91b 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -1,6 +1,6 @@ { "name": "dokploy", - "version": "v0.25.6", + "version": "v0.25.11", "private": true, "license": "Apache-2.0", "type": "module", @@ -98,6 +98,7 @@ "bl": "6.0.11", "boxen": "^7.1.1", "bullmq": "5.4.2", + "shell-quote": "^1.8.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^0.2.1", @@ -120,7 +121,7 @@ "next": "^15.3.2", "next-i18next": "^15.4.2", "next-themes": "^0.2.1", - "node-os-utils": "1.3.7", + "node-os-utils": "2.0.1", "node-pty": "1.0.0", "node-schedule": "2.1.1", "nodemailer": "6.9.14", @@ -157,13 +158,13 @@ "zod-form-data": "^2.0.7" }, "devDependencies": { + "@types/shell-quote": "^1.7.5", "@types/adm-zip": "^0.5.7", "@types/bcrypt": "5.0.2", "@types/js-cookie": "^3.0.6", "@types/lodash": "4.17.4", "@types/micromatch": "4.0.9", "@types/node": "^18.19.104", - "@types/node-os-utils": "1.3.4", "@types/node-schedule": "2.1.6", "@types/nodemailer": "^6.4.17", "@types/qrcode": "^1.5.5", diff --git a/apps/dokploy/pages/api/deploy/[refreshToken].ts b/apps/dokploy/pages/api/deploy/[refreshToken].ts index fd01c81fa..2ab607736 100644 --- a/apps/dokploy/pages/api/deploy/[refreshToken].ts +++ b/apps/dokploy/pages/api/deploy/[refreshToken].ts @@ -12,6 +12,17 @@ import type { DeploymentJob } from "@/server/queues/queue-types"; import { myQueue } from "@/server/queues/queueSetup"; import { deploy } from "@/server/utils/deploy"; +/** + * Helper function to get package_version from registry_package events + */ +const getPackageVersion = (headers: any, body: any) => { + const event = headers["x-github-event"]; + if (event === "registry_package") { + return body.registry_package?.package_version; + } + return null; +}; + export default async function handler( req: NextApiRequest, res: NextApiResponse, @@ -46,21 +57,60 @@ export default async function handler( } const deploymentTitle = extractCommitMessage(req.headers, req.body); - const deploymentHash = extractHash(req.headers, req.body); + const deploymentHash = extractHash(req.headers, req.body); const sourceType = application.sourceType; if (sourceType === "docker") { + const applicationImageName = extractImageName(application.dockerImage); const applicationDockerTag = extractImageTag(application.dockerImage); + + const webhookImageName = extractImageNameFromRequest( + req.headers, + req.body, + ); const webhookDockerTag = extractImageTagFromRequest( req.headers, req.body, ); - if ( - applicationDockerTag && - webhookDockerTag && - webhookDockerTag !== applicationDockerTag - ) { + + if (!applicationImageName) { + res.status(301).json({ + message: "Application Docker Image Name Not Found", + }); + return; + } + + if (!webhookImageName) { + res.status(301).json({ + message: "Webhook Docker Image Name Not Found", + }); + return; + } + + // Validate image name matches + if (webhookImageName !== applicationImageName) { + res.status(301).json({ + message: `Application Image Name (${applicationImageName}) doesn't match request event payload Image Name (${webhookImageName}).`, + }); + return; + } + + if (!applicationDockerTag) { + res.status(301).json({ + message: "Application Docker Tag Not Found", + }); + return; + } + + if (!webhookDockerTag) { + res.status(301).json({ + message: "Webhook Docker Tag Not Found", + }); + return; + } + + if (webhookDockerTag !== applicationDockerTag) { res.status(301).json({ message: `Application Image Tag (${applicationDockerTag}) doesn't match request event payload Image Tag (${webhookDockerTag}).`, }); @@ -191,7 +241,7 @@ export default async function handler( const jobData: DeploymentJob = { applicationId: application.applicationId as string, titleLog: deploymentTitle, - descriptionLog: `Hash: ${deploymentHash}`, + ...(deploymentHash && { descriptionLog: `Hash: ${deploymentHash}` }), type: "deploy", applicationType: "application", server: !!application.serverId, @@ -222,6 +272,39 @@ export default async function handler( } } +/** + * Return the image name without the tag + * Example: "my-image" => "my-image" + * Example: "my-image:latest" => "my-image" + * Example: "my-image:1.0.0" => "my-image" + * Example: "myregistryhost:5000/fedora/httpd:version1.0" => "myregistryhost:5000/fedora/httpd" + * @link https://docs.docker.com/reference/cli/docker/image/tag/ + */ +export function extractImageName(dockerImage: string | null): string | null { + if (!dockerImage || typeof dockerImage !== "string") { + return null; + } + + // Handle case where there's no tag (no colon or colon is part of port number) + const lastColonIndex = dockerImage.lastIndexOf(":"); + if (lastColonIndex === -1) { + return dockerImage; + } + + // Check if the part after the last colon looks like a tag (not a port number) + // Port numbers are typically 1-5 digits, tags are usually longer or contain letters + const afterColon = dockerImage.substring(lastColonIndex + 1); + const isPortNumber = /^\d{1,5}$/.test(afterColon); + + // If it's a port number (like registry:5000/image), don't split + if (isPortNumber) { + return dockerImage; + } + + // Otherwise, split at the last colon to get image name + return dockerImage.substring(0, lastColonIndex); +} + /** * Return the last part of the image name, which is the tag * Example: "my-image" => null @@ -230,7 +313,7 @@ export default async function handler( * Example: "myregistryhost:5000/fedora/httpd:version1.0" => "version1.0" * @link https://docs.docker.com/reference/cli/docker/image/tag/ */ -function extractImageTag(dockerImage: string | null) { +export function extractImageTag(dockerImage: string | null) { if (!dockerImage || typeof dockerImage !== "string") { return null; } @@ -240,12 +323,78 @@ function extractImageTag(dockerImage: string | null) { } /** + * Extract the image name (without tag) from webhook request * @link https://docs.docker.com/docker-hub/webhooks/#example-webhook-payload + * @link https://docs.github.com/en/webhooks/webhook-events-and-payloads#registry_package + */ +export const extractImageNameFromRequest = ( + headers: any, + body: any, +): string | null => { + // GitHub Packages: registry_package events (container registry) + const packageVersion = getPackageVersion(headers, body); + if (packageVersion?.package_url) { + const packageUrl = packageVersion.package_url; + // Remove tag if present (everything after the last colon) + if (packageUrl.includes(":")) { + const lastColonIndex = packageUrl.lastIndexOf(":"); + // Check if it's a port number (like registry:5000/image) + const afterColon = packageUrl.substring(lastColonIndex + 1); + const isPortNumber = /^\d{1,5}$/.test(afterColon); + if (isPortNumber) { + return packageUrl; + } + return packageUrl.substring(0, lastColonIndex); + } + return packageUrl; + } + + // Docker Hub + if (headers["user-agent"]?.includes("Go-http-client")) { + if (body.repository) { + const repoName = body.repository.repo_name; + return `${repoName}`; + } + } + return null; +}; + +/** + * @link https://docs.docker.com/docker-hub/webhooks/#example-webhook-payload + * @link https://docs.github.com/en/webhooks/webhook-events-and-payloads#registry_package */ export const extractImageTagFromRequest = ( headers: any, body: any, ): string | null => { + // GitHub Packages: registry_package events (container registry) + const packageVersion = getPackageVersion(headers, body); + if (packageVersion) { + // Try to get tag from container_metadata first (most reliable) + // Only use it if it's not empty and not the same as the version (digest) + const tagName = packageVersion.container_metadata?.tag?.name?.trim() || ""; + if ( + tagName && + tagName !== packageVersion.version && + !tagName.startsWith("sha256:") + ) { + return tagName; + } + // Fallback: extract tag from package_url (e.g., "ghcr.io/owner/repo:tag") + if (packageVersion.package_url) { + const packageUrl = packageVersion.package_url; + // Handle case where package_url ends with colon (no tag) + if (packageUrl.endsWith(":")) { + return null; + } + const tagMatch = packageUrl.match(/:([^:]+)$/); + if (tagMatch?.[1]?.trim()) { + return tagMatch[1].trim(); + } + } + } + + // Docker Hub if (headers["user-agent"]?.includes("Go-http-client")) { if (body.push_data && body.repository) { return body.push_data.tag; @@ -255,6 +404,18 @@ export const extractImageTagFromRequest = ( }; export const extractCommitMessage = (headers: any, body: any) => { + // GitHub Packages: registry_package events (container tags) + const githubEvent = headers["x-github-event"]; + if (githubEvent === "registry_package") { + const packageVersion = getPackageVersion(headers, body); + if (packageVersion) { + if (packageVersion.package_url) { + return `Docker GHCR image pushed: ${packageVersion.package_url}`; + } + return "Docker GHCR image pushed"; + } + // If package_version is missing, fall through to default behavior + } // GitHub if (headers["x-github-event"]) { return body.head_commit ? body.head_commit.message : "NEW COMMIT"; @@ -283,7 +444,7 @@ export const extractCommitMessage = (headers: any, body: any) => { if (headers["user-agent"]?.includes("Go-http-client")) { if (body.push_data && body.repository) { - return `Docker image pushed: ${body.repository.repo_name}:${body.push_data.tag} by ${body.push_data.pusher}`; + return `DockerHub image pushed: ${body.repository.repo_name}:${body.push_data.tag} by ${body.push_data.pusher}`; } } diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx index c09111d20..a2e54ad51 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx @@ -1,4 +1,4 @@ -import type { findProjectById } from "@dokploy/server"; +import type { findEnvironmentById } from "@dokploy/server"; import { validateRequest } from "@dokploy/server/lib/auth"; import { createServerSideHelpers } from "@trpc/react-query/server"; import { @@ -102,6 +102,7 @@ import { api } from "@/utils/api"; export type Services = { appName: string; serverId?: string | null; + serverName?: string | null; name: string; type: | "mariadb" @@ -115,10 +116,10 @@ export type Services = { id: string; createdAt: string; status?: "idle" | "running" | "done" | "error"; + lastDeployDate?: Date | null; }; -type Project = Awaited>; -type Environment = Project["environments"][0]; +type Environment = Awaited>; export const extractServicesFromEnvironment = ( environment: Environment | undefined, @@ -128,16 +129,35 @@ export const extractServicesFromEnvironment = ( const allServices: Services[] = []; const applications: Services[] = - environment.applications?.map((item) => ({ - appName: item.appName, - name: item.name, - type: "application", - id: item.applicationId, - createdAt: item.createdAt, - status: item.applicationStatus, - description: item.description, - serverId: item.serverId, - })) || []; + environment.applications?.map((item) => { + // Get the most recent deployment date + let lastDeployDate: Date | null = null; + const deployments = (item as any).deployments; + if (deployments && deployments.length > 0) { + for (const deployment of deployments) { + const deployDate = new Date( + deployment.finishedAt || + deployment.startedAt || + deployment.createdAt, + ); + if (!lastDeployDate || deployDate > lastDeployDate) { + lastDeployDate = deployDate; + } + } + } + return { + appName: item.appName, + name: item.name, + type: "application", + id: item.applicationId, + createdAt: item.createdAt, + status: item.applicationStatus, + description: item.description, + serverId: item.serverId, + serverName: item?.server?.name || null, + lastDeployDate, + }; + }) || []; const mariadb: Services[] = environment.mariadb?.map((item) => ({ @@ -149,6 +169,7 @@ export const extractServicesFromEnvironment = ( status: item.applicationStatus, description: item.description, serverId: item.serverId, + serverName: item?.server?.name || null, })) || []; const postgres: Services[] = @@ -161,6 +182,7 @@ export const extractServicesFromEnvironment = ( status: item.applicationStatus, description: item.description, serverId: item.serverId, + serverName: item?.server?.name || null, })) || []; const mongo: Services[] = @@ -173,6 +195,7 @@ export const extractServicesFromEnvironment = ( status: item.applicationStatus, description: item.description, serverId: item.serverId, + serverName: item?.server?.name || null, })) || []; const redis: Services[] = @@ -185,6 +208,7 @@ export const extractServicesFromEnvironment = ( status: item.applicationStatus, description: item.description, serverId: item.serverId, + serverName: item?.server?.name || null, })) || []; const mysql: Services[] = @@ -197,19 +221,39 @@ export const extractServicesFromEnvironment = ( status: item.applicationStatus, description: item.description, serverId: item.serverId, + serverName: item?.server?.name || null, })) || []; const compose: Services[] = - environment.compose?.map((item) => ({ - appName: item.appName, - name: item.name, - type: "compose", - id: item.composeId, - createdAt: item.createdAt, - status: item.composeStatus, - description: item.description, - serverId: item.serverId, - })) || []; + environment.compose?.map((item) => { + // Get the most recent deployment date + let lastDeployDate: Date | null = null; + const deployments = (item as any).deployments; + if (deployments && deployments.length > 0) { + for (const deployment of deployments) { + const deployDate = new Date( + deployment.finishedAt || + deployment.startedAt || + deployment.createdAt, + ); + if (!lastDeployDate || deployDate > lastDeployDate) { + lastDeployDate = deployDate; + } + } + } + return { + appName: item.appName, + name: item.name, + type: "compose", + id: item.composeId, + createdAt: item.createdAt, + status: item.composeStatus, + description: item.description, + serverId: item.serverId, + serverName: item?.server?.name || null, + lastDeployDate, + }; + }) || []; allServices.push( ...applications, @@ -237,9 +281,9 @@ const EnvironmentPage = ( const { data: auth } = api.user.get.useQuery(); const [sortBy, setSortBy] = useState(() => { if (typeof window !== "undefined") { - return localStorage.getItem("servicesSort") || "createdAt-desc"; + return localStorage.getItem("servicesSort") || "lastDeploy-desc"; } - return "createdAt-desc"; + return "lastDeploy-desc"; }); useEffect(() => { @@ -261,10 +305,45 @@ const EnvironmentPage = ( comparison = new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); break; + case "lastDeploy": { + const aLastDeploy = a.lastDeployDate; + const bLastDeploy = b.lastDeployDate; + + if (direction === "desc") { + // For "desc" (newest first): services with deployments first, then those without + if (!aLastDeploy && !bLastDeploy) { + comparison = 0; + } else if (!aLastDeploy) { + comparison = 1; // a (no deploy) goes after b (has deploy) + } else if (!bLastDeploy) { + comparison = -1; // a (has deploy) goes before b (no deploy) + } else { + // Both have deployments: newest first (negative if a is newer) + comparison = bLastDeploy.getTime() - aLastDeploy.getTime(); + } + } else { + // For "asc" (oldest first): services with deployments first, then those without + if (!aLastDeploy && !bLastDeploy) { + comparison = 0; + } else if (!aLastDeploy) { + comparison = 1; // a (no deploy) goes after b (has deploy) + } else if (!bLastDeploy) { + comparison = -1; // a (has deploy) goes before b (no deploy) + } else { + // Both have deployments: oldest first + comparison = aLastDeploy.getTime() - bLastDeploy.getTime(); + } + } + break; + } default: comparison = 0; } - return direction === "asc" ? comparison : -comparison; + // For other fields, apply direction normally + if (field !== "lastDeploy") { + return direction === "asc" ? comparison : -comparison; + } + return comparison; }); }; @@ -320,6 +399,7 @@ const EnvironmentPage = ( const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false); const [deleteVolumes, setDeleteVolumes] = useState(false); + const [selectedServerId, setSelectedServerId] = useState("all"); const handleSelectAll = () => { if (selectedServices.length === filteredServices.length) { @@ -709,6 +789,27 @@ const EnvironmentPage = ( setIsBulkActionLoading(false); }; + // Get unique servers from services + const availableServers = useMemo(() => { + if (!applications) return []; + const servers = new Map(); + applications.forEach((service) => { + if (service.serverId && service.serverName) { + servers.set(service.serverId, { + serverId: service.serverId, + serverName: service.serverName, + }); + } + }); + return Array.from(servers.values()); + }, [applications]); + + // Check if there are services without a server (Dokploy server) + const hasServicesWithoutServer = useMemo(() => { + if (!applications) return false; + return applications.some((service) => !service.serverId); + }, [applications]); + const filteredServices = useMemo(() => { if (!applications) return []; const filtered = applications.filter( @@ -717,10 +818,14 @@ const EnvironmentPage = ( service.description ?.toLowerCase() .includes(searchQuery.toLowerCase())) && - (selectedTypes.length === 0 || selectedTypes.includes(service.type)), + (selectedTypes.length === 0 || selectedTypes.includes(service.type)) && + (selectedServerId === "" || + selectedServerId === "all" || + (selectedServerId === "dokploy-server" && !service.serverId) || + service.serverId === selectedServerId), ); return sortServices(filtered); - }, [applications, searchQuery, selectedTypes, sortBy]); + }, [applications, searchQuery, selectedTypes, selectedServerId, sortBy]); const selectedServicesWithRunningStatus = useMemo(() => { return filteredServices.filter( @@ -1217,6 +1322,9 @@ const EnvironmentPage = ( + + Recently deployed + Newest first @@ -1291,6 +1399,39 @@ const EnvironmentPage = ( + {(availableServers.length > 0 || + hasServicesWithoutServer) && ( + + )} @@ -1396,7 +1537,15 @@ const EnvironmentPage = ( -
+
+ {service.serverName && ( +
+ + + {service.serverName} + +
+ )} Created diff --git a/apps/dokploy/server/api/routers/application.ts b/apps/dokploy/server/api/routers/application.ts index 006d024c4..c713fd7eb 100644 --- a/apps/dokploy/server/api/routers/application.ts +++ b/apps/dokploy/server/api/routers/application.ts @@ -58,7 +58,11 @@ import { applications, } from "@/server/db/schema"; 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 { uploadFileSchema } from "@/utils/schema"; @@ -725,7 +729,21 @@ export const applicationRouter = createTRPCRouter({ } 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 .input(apiFindOneApplication) .query(async ({ input, ctx }) => { diff --git a/apps/dokploy/server/api/routers/compose.ts b/apps/dokploy/server/api/routers/compose.ts index e2f25b763..e233dc6ca 100644 --- a/apps/dokploy/server/api/routers/compose.ts +++ b/apps/dokploy/server/api/routers/compose.ts @@ -3,13 +3,14 @@ import { addNewService, checkServiceAccess, cloneCompose, - cloneComposeRemote, createCommand, createCompose, createComposeByTemplate, createDomain, createMount, deleteMount, + execAsync, + execAsyncRemote, findComposeById, findDomainsByComposeId, findEnvironmentById, @@ -58,7 +59,11 @@ import { compose as composeTable, } from "@/server/db/schema"; 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 { generatePassword } from "@/templates/utils"; import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc"; @@ -247,6 +252,21 @@ export const composeRouter = createTRPCRouter({ await cleanQueuesByCompose(input.composeId); 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 .input(apiFetchServices) @@ -302,10 +322,12 @@ export const composeRouter = createTRPCRouter({ message: "You are not authorized to fetch this compose", }); } + + const command = await cloneCompose(compose); if (compose.serverId) { - await cloneComposeRemote(compose); + await execAsyncRemote(compose.serverId, command); } else { - await cloneCompose(compose); + await execAsync(command); } return compose.sourceType; } catch (err) { diff --git a/apps/dokploy/server/api/routers/destination.ts b/apps/dokploy/server/api/routers/destination.ts index dc5892c85..d7cbf53d5 100644 --- a/apps/dokploy/server/api/routers/destination.ts +++ b/apps/dokploy/server/api/routers/destination.ts @@ -47,15 +47,19 @@ export const destinationRouter = createTRPCRouter({ input; try { const rcloneFlags = [ - `--s3-access-key-id=${accessKey}`, - `--s3-secret-access-key=${secretAccessKey}`, - `--s3-region=${region}`, - `--s3-endpoint=${endpoint}`, + `--s3-access-key-id="${accessKey}"`, + `--s3-secret-access-key="${secretAccessKey}"`, + `--s3-region="${region}"`, + `--s3-endpoint="${endpoint}"`, "--s3-no-check-bucket", "--s3-force-path-style", + "--retries 1", + "--low-level-retries 1", + "--timeout 10s", + "--contimeout 5s", ]; if (provider) { - rcloneFlags.unshift(`--s3-provider=${provider}`); + rcloneFlags.unshift(`--s3-provider="${provider}"`); } const rcloneDestination = `:s3:${bucket}`; const rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} "${rcloneDestination}"`; diff --git a/apps/dokploy/server/api/routers/notification.ts b/apps/dokploy/server/api/routers/notification.ts index 0edba5732..14d5b8363 100644 --- a/apps/dokploy/server/api/routers/notification.ts +++ b/apps/dokploy/server/api/routers/notification.ts @@ -117,7 +117,7 @@ export const notificationRouter = createTRPCRouter({ } catch (error) { throw new TRPCError({ code: "BAD_REQUEST", - message: "Error testing the notification", + message: `${error instanceof Error ? error.message : "Unknown error"}`, cause: error, }); } @@ -234,7 +234,7 @@ export const notificationRouter = createTRPCRouter({ } catch (error) { throw new TRPCError({ code: "BAD_REQUEST", - message: "Error testing the notification", + message: `${error instanceof Error ? error.message : "Unknown error"}`, cause: error, }); } @@ -291,7 +291,7 @@ export const notificationRouter = createTRPCRouter({ } catch (error) { throw new TRPCError({ code: "BAD_REQUEST", - message: "Error testing the notification", + message: `${error instanceof Error ? error.message : "Unknown error"}`, cause: error, }); } diff --git a/apps/dokploy/server/api/routers/postgres.ts b/apps/dokploy/server/api/routers/postgres.ts index a05072ab7..3112beb66 100644 --- a/apps/dokploy/server/api/routers/postgres.ts +++ b/apps/dokploy/server/api/routers/postgres.ts @@ -8,6 +8,7 @@ import { findEnvironmentById, findPostgresById, findProjectById, + getMountPath, IS_CLOUD, rebuildDatabase, removePostgresById, @@ -37,6 +38,7 @@ import { postgres as postgresTable, } from "@/server/db/schema"; import { cancelJobs } from "@/server/utils/backup"; + export const postgresRouter = createTRPCRouter({ create: protectedProcedure .input(apiCreatePostgres) @@ -79,11 +81,13 @@ export const postgresRouter = createTRPCRouter({ ); } + const mountPath = getMountPath(input.dockerImage); + await createMount({ serviceId: newPostgres.postgresId, serviceType: "postgres", volumeName: `${newPostgres.appName}-data`, - mountPath: "/var/lib/postgresql/data", + mountPath: mountPath, type: "volume", }); @@ -282,12 +286,16 @@ export const postgresRouter = createTRPCRouter({ const backups = await findBackupsByDbId(input.postgresId, "postgres"); const cleanupOperations = [ - removeService(postgres.appName, postgres.serverId), - cancelJobs(backups), - removePostgresById(input.postgresId), + async () => await removeService(postgres?.appName, postgres.serverId), + async () => await cancelJobs(backups), + async () => await removePostgresById(input.postgresId), ]; - await Promise.allSettled(cleanupOperations); + for (const operation of cleanupOperations) { + try { + await operation(); + } catch (_) {} + } return postgres; }), @@ -363,6 +371,7 @@ export const postgresRouter = createTRPCRouter({ message: "You are not authorized to update this Postgres", }); } + const service = await updatePostgresById(postgresId, { ...rest, }); diff --git a/apps/dokploy/server/api/routers/server.ts b/apps/dokploy/server/api/routers/server.ts index d6904a7ec..8a01228f8 100644 --- a/apps/dokploy/server/api/routers/server.ts +++ b/apps/dokploy/server/api/routers/server.ts @@ -383,6 +383,15 @@ export const serverRouter = createTRPCRouter({ const ip = await getPublicIpWithFallback(); return ip; }), + getServerTime: protectedProcedure.query(() => { + if (IS_CLOUD) { + return null; + } + return { + time: new Date(), + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + }; + }), getServerMetrics: protectedProcedure .input( z.object({ diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts index b4968c260..e0a74f5cf 100644 --- a/apps/dokploy/server/api/routers/settings.ts +++ b/apps/dokploy/server/api/routers/settings.ts @@ -587,7 +587,7 @@ export const settingsRouter = createTRPCRouter({ return ports.some((port) => port.targetPort === 8080); }), - readStatsLogs: adminProcedure + readStatsLogs: protectedProcedure .meta({ openapi: { path: "/read-stats-logs", @@ -650,7 +650,7 @@ export const settingsRouter = createTRPCRouter({ const processedLogs = processLogs(rawConfig as string, input?.dateRange); return processedLogs || []; }), - haveActivateRequests: adminProcedure.query(async () => { + haveActivateRequests: protectedProcedure.query(async () => { if (IS_CLOUD) { return true; } @@ -665,7 +665,7 @@ export const settingsRouter = createTRPCRouter({ return !!parsedConfig?.accessLog?.filePath; }), - toggleRequests: adminProcedure + toggleRequests: protectedProcedure .input( z.object({ enable: z.boolean(), @@ -835,7 +835,7 @@ export const settingsRouter = createTRPCRouter({ const ports = await readPorts("dokploy-traefik", input?.serverId); return ports; }), - updateLogCleanup: adminProcedure + updateLogCleanup: protectedProcedure .input( z.object({ cronExpression: z.string().nullable(), @@ -851,7 +851,7 @@ export const settingsRouter = createTRPCRouter({ return stopLogCleanup(); }), - getLogCleanupStatus: adminProcedure.query(async () => { + getLogCleanupStatus: protectedProcedure.query(async () => { return getLogCleanupStatus(); }), diff --git a/apps/dokploy/server/api/routers/user.ts b/apps/dokploy/server/api/routers/user.ts index d30b99b3a..baec4d6ac 100644 --- a/apps/dokploy/server/api/routers/user.ts +++ b/apps/dokploy/server/api/routers/user.ts @@ -4,6 +4,7 @@ import { findNotificationById, findOrganizationById, findUserById, + getDokployUrl, getUserByToken, IS_CLOUD, removeUserById, @@ -419,11 +420,10 @@ export const userRouter = createTRPCRouter({ }); } - const admin = await findAdmin(); const host = process.env.NODE_ENV === "development" ? "http://localhost:3000" - : admin.user.host; + : await getDokployUrl(); const inviteLink = `${host}/invitation?token=${input.invitationId}`; const organization = await findOrganizationById( diff --git a/apps/dokploy/server/queues/deployments-queue.ts b/apps/dokploy/server/queues/deployments-queue.ts index b8dfb8cd0..4c117e7e3 100644 --- a/apps/dokploy/server/queues/deployments-queue.ts +++ b/apps/dokploy/server/queues/deployments-queue.ts @@ -2,13 +2,8 @@ import { deployApplication, deployCompose, deployPreviewApplication, - deployRemoteApplication, - deployRemoteCompose, - deployRemotePreviewApplication, rebuildApplication, rebuildCompose, - rebuildRemoteApplication, - rebuildRemoteCompose, updateApplicationStatus, updateCompose, updatePreviewDeployment, @@ -24,91 +19,48 @@ export const deploymentWorker = new Worker( if (job.data.applicationType === "application") { await updateApplicationStatus(job.data.applicationId, "running"); - if (job.data.server) { - if (job.data.type === "redeploy") { - await rebuildRemoteApplication({ - applicationId: job.data.applicationId, - titleLog: job.data.titleLog, - descriptionLog: job.data.descriptionLog, - }); - } else if (job.data.type === "deploy") { - await deployRemoteApplication({ - applicationId: job.data.applicationId, - titleLog: job.data.titleLog, - descriptionLog: job.data.descriptionLog, - }); - } - } else { - if (job.data.type === "redeploy") { - await rebuildApplication({ - applicationId: job.data.applicationId, - titleLog: job.data.titleLog, - descriptionLog: job.data.descriptionLog, - }); - } else if (job.data.type === "deploy") { - await deployApplication({ - applicationId: job.data.applicationId, - titleLog: job.data.titleLog, - descriptionLog: job.data.descriptionLog, - }); - } + if (job.data.type === "redeploy") { + await rebuildApplication({ + applicationId: job.data.applicationId, + titleLog: job.data.titleLog, + descriptionLog: job.data.descriptionLog, + }); + } else if (job.data.type === "deploy") { + await deployApplication({ + applicationId: job.data.applicationId, + titleLog: job.data.titleLog, + descriptionLog: job.data.descriptionLog, + }); } } else if (job.data.applicationType === "compose") { await updateCompose(job.data.composeId, { composeStatus: "running", }); - - if (job.data.server) { - if (job.data.type === "redeploy") { - await rebuildRemoteCompose({ - composeId: job.data.composeId, - titleLog: job.data.titleLog, - descriptionLog: job.data.descriptionLog, - }); - } else if (job.data.type === "deploy") { - await deployRemoteCompose({ - composeId: job.data.composeId, - titleLog: job.data.titleLog, - descriptionLog: job.data.descriptionLog, - }); - } - } else { - if (job.data.type === "deploy") { - await deployCompose({ - composeId: job.data.composeId, - titleLog: job.data.titleLog, - descriptionLog: job.data.descriptionLog, - }); - } else if (job.data.type === "redeploy") { - await rebuildCompose({ - composeId: job.data.composeId, - titleLog: job.data.titleLog, - descriptionLog: job.data.descriptionLog, - }); - } + if (job.data.type === "deploy") { + await deployCompose({ + composeId: job.data.composeId, + titleLog: job.data.titleLog, + descriptionLog: job.data.descriptionLog, + }); + } else if (job.data.type === "redeploy") { + await rebuildCompose({ + composeId: job.data.composeId, + titleLog: job.data.titleLog, + descriptionLog: job.data.descriptionLog, + }); } } else if (job.data.applicationType === "application-preview") { await updatePreviewDeployment(job.data.previewDeploymentId, { previewStatus: "running", }); - if (job.data.server) { - if (job.data.type === "deploy") { - await deployRemotePreviewApplication({ - applicationId: job.data.applicationId, - titleLog: job.data.titleLog, - descriptionLog: job.data.descriptionLog, - previewDeploymentId: job.data.previewDeploymentId, - }); - } - } else { - if (job.data.type === "deploy") { - await deployPreviewApplication({ - applicationId: job.data.applicationId, - titleLog: job.data.titleLog, - descriptionLog: job.data.descriptionLog, - previewDeploymentId: job.data.previewDeploymentId, - }); - } + + if (job.data.type === "deploy") { + await deployPreviewApplication({ + applicationId: job.data.applicationId, + titleLog: job.data.titleLog, + descriptionLog: job.data.descriptionLog, + previewDeploymentId: job.data.previewDeploymentId, + }); } } } catch (error) { diff --git a/apps/dokploy/server/queues/queueSetup.ts b/apps/dokploy/server/queues/queueSetup.ts index 1577273c8..351f5d1c0 100644 --- a/apps/dokploy/server/queues/queueSetup.ts +++ b/apps/dokploy/server/queues/queueSetup.ts @@ -1,3 +1,7 @@ +import { + execAsync, + execAsyncRemote, +} from "@dokploy/server/utils/process/execAsync"; import { Queue } from "bullmq"; 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 }; diff --git a/apps/dokploy/server/wss/docker-stats.ts b/apps/dokploy/server/wss/docker-stats.ts index 99e993dce..bd740e976 100644 --- a/apps/dokploy/server/wss/docker-stats.ts +++ b/apps/dokploy/server/wss/docker-stats.ts @@ -2,6 +2,7 @@ import type http from "node:http"; import { docker, execAsync, + getHostSystemStats, getLastAdvancedStatsFile, recordAdvancedStats, validateRequest, @@ -49,6 +50,21 @@ export const setupDockerStatsMonitoringSocketServer = ( } const intervalId = setInterval(async () => { try { + // Special case: when monitoring "dokploy", get host system stats instead of container stats + if (appName === "dokploy") { + const stat = await getHostSystemStats(); + + await recordAdvancedStats(stat, appName); + const data = await getLastAdvancedStatsFile(appName); + + ws.send( + JSON.stringify({ + data, + }), + ); + return; + } + const filter = { status: ["running"], ...(appType === "application" && { diff --git a/apps/dokploy/setup.ts b/apps/dokploy/setup.ts index 55e1da87c..e0ccb86d8 100644 --- a/apps/dokploy/setup.ts +++ b/apps/dokploy/setup.ts @@ -22,7 +22,7 @@ import { await initializeNetwork(); createDefaultTraefikConfig(); createDefaultServerTraefikConfig(); - await execAsync("docker pull traefik:v3.5.0"); + await execAsync("docker pull traefik:v3.6.1"); await initializeStandaloneTraefik(); await initializeRedis(); await initializePostgres(); diff --git a/packages/server/package.json b/packages/server/package.json index dcdc57e79..e23fa6d8b 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -62,7 +62,7 @@ "lodash": "4.17.21", "micromatch": "4.0.8", "nanoid": "3.3.11", - "node-os-utils": "1.3.7", + "node-os-utils": "2.0.1", "node-pty": "1.0.0", "node-schedule": "2.1.1", "nodemailer": "6.9.14", @@ -76,6 +76,7 @@ "react": "18.2.0", "react-dom": "18.2.0", "rotating-file-stream": "3.2.3", + "shell-quote": "^1.8.1", "slugify": "^1.6.6", "ssh2": "1.15.0", "toml": "3.0.0", @@ -89,12 +90,12 @@ "@types/lodash": "4.17.4", "@types/micromatch": "4.0.9", "@types/node": "^18.19.104", - "@types/node-os-utils": "1.3.4", "@types/node-schedule": "2.1.6", "@types/nodemailer": "^6.4.17", "@types/qrcode": "^1.5.5", "@types/react": "^18.3.5", "@types/react-dom": "^18.3.0", + "@types/shell-quote": "^1.7.5", "@types/ssh2": "1.15.1", "@types/ws": "8.5.10", "drizzle-kit": "^0.30.6", diff --git a/packages/server/src/emails/emails/build-success.tsx b/packages/server/src/emails/emails/build-success.tsx index d9e500ab9..e5e1d1bb4 100644 --- a/packages/server/src/emails/emails/build-success.tsx +++ b/packages/server/src/emails/emails/build-success.tsx @@ -19,6 +19,7 @@ export type TemplateProps = { applicationType: string; buildLink: string; date: string; + environmentName: string; }; export const BuildSuccessEmail = ({ @@ -27,6 +28,7 @@ export const BuildSuccessEmail = ({ applicationType = "application", buildLink = "https://dokploy.com/projects/dokploy-test/applications/dokploy-test", date = "2023-05-01T00:00:00.000Z", + environmentName = "production", }: TemplateProps) => { const previewText = `Build success for ${applicationName}`; return ( @@ -74,6 +76,9 @@ export const BuildSuccessEmail = ({ Application Name: {applicationName} + + Environment: {environmentName} + Application Type: {applicationType} diff --git a/packages/server/src/monitoring/utils.ts b/packages/server/src/monitoring/utils.ts index 11ebb6169..2c42b99a6 100644 --- a/packages/server/src/monitoring/utils.ts +++ b/packages/server/src/monitoring/utils.ts @@ -1,5 +1,5 @@ import { promises } from "node:fs"; -import osUtils from "node-os-utils"; +import { OSUtils } from "node-os-utils"; import { paths } from "../constants"; export interface Container { @@ -38,22 +38,122 @@ export const recordAdvancedStats = async ( }); if (appName === "dokploy") { - const disk = await osUtils.drive.info("/"); + const osutils = new OSUtils(); + const diskResult = await osutils.disk.usageByMountPoint("/"); - const diskUsage = disk.usedGb; - const diskTotal = disk.totalGb; - const diskUsedPercentage = disk.usedPercentage; - const diskFree = disk.freeGb; + if (diskResult.success && diskResult.data) { + const disk = diskResult.data; + const diskUsage = disk.used.toGB().toFixed(2); + const diskTotal = disk.total.toGB().toFixed(2); + const diskUsedPercentage = disk.usagePercentage; + const diskFree = disk.available.toGB().toFixed(2); - await updateStatsFile(appName, "disk", { - diskTotal: +diskTotal, - diskUsedPercentage: +diskUsedPercentage, - diskUsage: +diskUsage, - diskFree: +diskFree, - }); + await updateStatsFile(appName, "disk", { + diskTotal: +diskTotal, + diskUsedPercentage: +diskUsedPercentage, + diskUsage: +diskUsage, + diskFree: +diskFree, + }); + } } }; +/** + * Get host system statistics using node-os-utils + * This is used when monitoring "dokploy" to show host stats instead of container stats + */ +export const getHostSystemStats = async (): Promise => { + const osutils = new OSUtils({ + disk: { + includeStats: true, // Enable disk I/O statistics + }, + }); + + // Get CPU usage + const cpuResult = await osutils.cpu.usage(); + const cpuUsage = cpuResult.success ? cpuResult.data : 0; + + // Get memory info + const memResult = await osutils.memory.info(); + let memUsedGB = 0; + let memTotalGB = 0; + let memUsedPercent = 0; + if (memResult.success) { + memTotalGB = memResult.data.total.toGB(); + memUsedGB = memResult.data.used.toGB(); + memUsedPercent = memResult.data.usagePercentage; + } + + // Get network stats from network.overview() + let netInputBytes = 0; + let netOutputBytes = 0; + const networkOverview = await osutils.network.overview(); + if (networkOverview.success) { + netInputBytes = networkOverview.data.totalRxBytes.toBytes(); + netOutputBytes = networkOverview.data.totalTxBytes.toBytes(); + } + + // Get Block I/O from disk.stats() + let blockReadBytes = 0; + let blockWriteBytes = 0; + const diskStats = await osutils.disk.stats(); + if (diskStats.success && diskStats.data.length > 0) { + // Filter out virtual devices (loop, ram, sr, etc.) - only include real disk devices + const excludePatterns = [/^loop/, /^ram/, /^sr\d+$/, /^fd\d+$/]; + for (const stat of diskStats.data) { + // Skip virtual devices + if ( + stat.device && + excludePatterns.some((pattern) => pattern.test(stat.device)) + ) { + continue; + } + // readBytes and writeBytes are DataSize objects with .toBytes() method + blockReadBytes += stat.readBytes.toBytes(); + blockWriteBytes += stat.writeBytes.toBytes(); + } + } + + // Format values similar to docker stats + const formatBytes = (bytes: number): string => { + if (bytes >= 1024 * 1024 * 1024) { + return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)}GiB`; + } + if (bytes >= 1024 * 1024) { + return `${(bytes / (1024 * 1024)).toFixed(2)}MiB`; + } + if (bytes >= 1024) { + return `${(bytes / 1024).toFixed(2)}KiB`; + } + return `${bytes}B`; + }; + + // Format memory usage similar to docker stats format: "used / total" + const memUsedFormatted = `${memUsedGB.toFixed(2)}GiB`; + const memTotalFormatted = `${memTotalGB.toFixed(2)}GiB`; + const memUsageFormatted = `${memUsedFormatted} / ${memTotalFormatted}`; + + // Format network I/O + const netInputMb = netInputBytes / (1024 * 1024); + const netOutputMb = netOutputBytes / (1024 * 1024); + const netIOFormatted = `${netInputMb.toFixed(2)}MB / ${netOutputMb.toFixed(2)}MB`; + + // Format Block I/O + const blockIOFormatted = `${formatBytes(blockReadBytes)} / ${formatBytes(blockWriteBytes)}`; + + // Create a stat object compatible with recordAdvancedStats + return { + CPUPerc: `${cpuUsage.toFixed(2)}%`, + MemPerc: `${memUsedPercent.toFixed(2)}%`, + MemUsage: memUsageFormatted, + BlockIO: blockIOFormatted, + NetIO: netIOFormatted, + Container: "dokploy", + ID: "host-system", + Name: "dokploy", + }; +}; + export const getAdvancedStats = async (appName: string) => { return { cpu: await readStatsFile(appName, "cpu"), diff --git a/packages/server/src/services/admin.ts b/packages/server/src/services/admin.ts index 55ee5caee..0cbb20785 100644 --- a/packages/server/src/services/admin.ts +++ b/packages/server/src/services/admin.ts @@ -110,7 +110,8 @@ export const getDokployUrl = async () => { const admin = await findAdmin(); if (admin.user.host) { - return `https://${admin.user.host}`; + const protocol = admin.user.https ? "https" : "http"; + return `${protocol}://${admin.user.host}`; } return `http://${admin.user.serverIp}:${process.env.PORT}`; }; diff --git a/packages/server/src/services/application.ts b/packages/server/src/services/application.ts index 1891f9b6b..6ad90a93b 100644 --- a/packages/server/src/services/application.ts +++ b/packages/server/src/services/application.ts @@ -7,37 +7,25 @@ import { } from "@dokploy/server/db/schema"; import { getAdvancedStats } from "@dokploy/server/monitoring/utils"; import { - buildApplication, getBuildCommand, mechanizeDockerContainer, } from "@dokploy/server/utils/builders"; import { sendBuildErrorNotifications } from "@dokploy/server/utils/notifications/build-error"; import { sendBuildSuccessNotifications } from "@dokploy/server/utils/notifications/build-success"; -import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync"; import { - cloneBitbucketRepository, - getBitbucketCloneCommand, -} from "@dokploy/server/utils/providers/bitbucket"; -import { - buildDocker, - buildRemoteDocker, -} from "@dokploy/server/utils/providers/docker"; + ExecError, + execAsync, + execAsyncRemote, +} from "@dokploy/server/utils/process/execAsync"; +import { cloneBitbucketRepository } from "@dokploy/server/utils/providers/bitbucket"; +import { buildRemoteDocker } from "@dokploy/server/utils/providers/docker"; import { cloneGitRepository, - getCustomGitCloneCommand, + getGitCommitInfo, } from "@dokploy/server/utils/providers/git"; -import { - cloneGiteaRepository, - getGiteaCloneCommand, -} from "@dokploy/server/utils/providers/gitea"; -import { - cloneGithubRepository, - getGithubCloneCommand, -} from "@dokploy/server/utils/providers/github"; -import { - cloneGitlabRepository, - getGitlabCloneCommand, -} from "@dokploy/server/utils/providers/gitlab"; +import { cloneGiteaRepository } from "@dokploy/server/utils/providers/gitea"; +import { cloneGithubRepository } from "@dokploy/server/utils/providers/github"; +import { cloneGitlabRepository } from "@dokploy/server/utils/providers/gitlab"; import { createTraefikConfig } from "@dokploy/server/utils/traefik/application"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; @@ -46,6 +34,7 @@ import { getDokployUrl } from "./admin"; import { createDeployment, createDeploymentPreview, + updateDeployment, updateDeploymentStatus, } from "./deployment"; import { type Domain, getDomainHost } from "./domain"; @@ -192,30 +181,31 @@ export const deployApplication = async ({ }); try { + let command = "set -e;"; if (application.sourceType === "github") { - await cloneGithubRepository({ - ...application, - logPath: deployment.logPath, - }); - await buildApplication(application, deployment.logPath); + command += await cloneGithubRepository(application); } else if (application.sourceType === "gitlab") { - await cloneGitlabRepository(application, deployment.logPath); - await buildApplication(application, deployment.logPath); + command += await cloneGitlabRepository(application); } else if (application.sourceType === "gitea") { - await cloneGiteaRepository(application, deployment.logPath); - await buildApplication(application, deployment.logPath); + command += await cloneGiteaRepository(application); } else if (application.sourceType === "bitbucket") { - await cloneBitbucketRepository(application, deployment.logPath); - await buildApplication(application, deployment.logPath); - } else if (application.sourceType === "docker") { - await buildDocker(application, deployment.logPath); + command += await cloneBitbucketRepository(application); } else if (application.sourceType === "git") { - await cloneGitRepository(application, deployment.logPath); - await buildApplication(application, deployment.logPath); - } else if (application.sourceType === "drop") { - await buildApplication(application, deployment.logPath); + command += await cloneGitRepository(application); + } else if (application.sourceType === "docker") { + command += await buildRemoteDocker(application); } + command += getBuildCommand(application); + + const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`; + if (application.serverId) { + await execAsyncRemote(application.serverId, commandWithLog); + } else { + await execAsync(commandWithLog); + } + + await mechanizeDockerContainer(application); await updateDeploymentStatus(deployment.deploymentId, "done"); await updateApplicationStatus(applicationId, "done"); @@ -237,8 +227,24 @@ export const deployApplication = async ({ buildLink, organizationId: application.environment.project.organizationId, domains: application.domains, + environmentName: application.environment.name, }); } 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 updateApplicationStatus(applicationId, "error"); @@ -253,8 +259,19 @@ export const deployApplication = async ({ }); throw error; - } + } finally { + // Only extract commit info for non-docker sources + if (application.sourceType !== "docker") { + const commitInfo = await getGitCommitInfo(application); + if (commitInfo) { + await updateDeployment(deployment.deploymentId, { + title: commitInfo.message, + description: `Commit: ${commitInfo.hash}`, + }); + } + } + } return true; }; @@ -268,50 +285,8 @@ export const rebuildApplication = async ({ descriptionLog: string; }) => { const application = await findApplicationById(applicationId); - - const deployment = await createDeployment({ - applicationId: applicationId, - title: titleLog, - description: descriptionLog, - }); - - try { - if (application.sourceType === "github") { - await buildApplication(application, deployment.logPath); - } else if (application.sourceType === "gitlab") { - await buildApplication(application, deployment.logPath); - } else if (application.sourceType === "bitbucket") { - await buildApplication(application, deployment.logPath); - } else if (application.sourceType === "docker") { - await buildDocker(application, deployment.logPath); - } else if (application.sourceType === "git") { - await buildApplication(application, deployment.logPath); - } else if (application.sourceType === "drop") { - await buildApplication(application, deployment.logPath); - } - await updateDeploymentStatus(deployment.deploymentId, "done"); - await updateApplicationStatus(applicationId, "done"); - } catch (error) { - await updateDeploymentStatus(deployment.deploymentId, "error"); - await updateApplicationStatus(applicationId, "error"); - throw error; - } - - return true; -}; - -export const deployRemoteApplication = async ({ - applicationId, - titleLog = "Manual deployment", - descriptionLog = "", -}: { - applicationId: string; - titleLog: string; - descriptionLog: string; -}) => { - 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({ applicationId: applicationId, title: titleLog, @@ -319,39 +294,16 @@ export const deployRemoteApplication = async ({ }); try { + let command = "set -e;"; + // Check case for docker only + command += getBuildCommand(application); + const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`; if (application.serverId) { - let command = "set -e;"; - if (application.sourceType === "github") { - command += await getGithubCloneCommand({ - ...application, - serverId: application.serverId, - logPath: deployment.logPath, - }); - } else if (application.sourceType === "gitlab") { - command += await getGitlabCloneCommand(application, deployment.logPath); - } else if (application.sourceType === "bitbucket") { - command += await getBitbucketCloneCommand( - application, - deployment.logPath, - ); - } else if (application.sourceType === "gitea") { - command += await getGiteaCloneCommand(application, deployment.logPath); - } else if (application.sourceType === "git") { - command += await getCustomGitCloneCommand( - application, - deployment.logPath, - ); - } else if (application.sourceType === "docker") { - command += await buildRemoteDocker(application, deployment.logPath); - } - - if (application.sourceType !== "docker") { - command += getBuildCommand(application, deployment.logPath); - } - await execAsyncRemote(application.serverId, command); - await mechanizeDockerContainer(application); + await execAsyncRemote(application.serverId, commandWithLog); + } else { + await execAsync(commandWithLog); } - + await mechanizeDockerContainer(application); await updateDeploymentStatus(deployment.deploymentId, "done"); await updateApplicationStatus(applicationId, "done"); @@ -373,32 +325,26 @@ export const deployRemoteApplication = async ({ buildLink, organizationId: application.environment.project.organizationId, domains: application.domains, + environmentName: application.environment.name, }); } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); + let command = ""; - const encodedContent = encodeBase64(errorMessage); - - await execAsyncRemote( - application.serverId, - ` - echo "\n\n===================================EXTRA LOGS============================================" >> ${deployment.logPath}; - echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath}; - echo "${encodedContent}" | base64 -d >> "${deployment.logPath}";`, - ); + // 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 updateApplicationStatus(applicationId, "error"); - - await sendBuildErrorNotifications({ - projectName: application.environment.project.name, - applicationName: application.name, - applicationType: "application", - errorMessage: `Please check the logs for details: ${errorMessage}`, - buildLink, - organizationId: application.environment.project.organizationId, - }); - throw error; } @@ -475,14 +421,22 @@ export const deployPreviewApplication = async ({ application.buildArgs = `${application.previewBuildArgs}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`; application.buildSecrets = `${application.previewBuildSecrets}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`; + let command = "set -e;"; if (application.sourceType === "github") { - await cloneGithubRepository({ + command += await cloneGithubRepository({ ...application, appName: previewDeployment.appName, branch: previewDeployment.branch, - logPath: deployment.logPath, }); - await buildApplication(application, deployment.logPath); + command += getBuildCommand(application); + + const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`; + if (application.serverId) { + await execAsyncRemote(application.serverId, commandWithLog); + } else { + await execAsync(commandWithLog); + } + await mechanizeDockerContainer(application); } const successComment = getIssueComment( application.name, @@ -513,170 +467,10 @@ export const deployPreviewApplication = async ({ return true; }; -export const deployRemotePreviewApplication = async ({ - applicationId, - titleLog = "Preview Deployment", - descriptionLog = "", - previewDeploymentId, -}: { - applicationId: string; - titleLog: string; - descriptionLog: string; - previewDeploymentId: string; -}) => { - const application = await findApplicationById(applicationId); - - const deployment = await createDeploymentPreview({ - title: titleLog, - description: descriptionLog, - previewDeploymentId: previewDeploymentId, - }); - - const previewDeployment = - await findPreviewDeploymentById(previewDeploymentId); - - await updatePreviewDeployment(previewDeploymentId, { - createdAt: new Date().toISOString(), - }); - - const previewDomain = getDomainHost(previewDeployment?.domain as Domain); - const issueParams = { - owner: application?.owner || "", - repository: application?.repository || "", - issue_number: previewDeployment.pullRequestNumber, - comment_id: Number.parseInt(previewDeployment.pullRequestCommentId), - githubId: application?.githubId || "", - }; - try { - const commentExists = await issueCommentExists({ - ...issueParams, - }); - if (!commentExists) { - const result = await createPreviewDeploymentComment({ - ...issueParams, - previewDomain, - appName: previewDeployment.appName, - githubId: application?.githubId || "", - previewDeploymentId, - }); - - if (!result) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Pull request comment not found", - }); - } - - issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId); - } - const buildingComment = getIssueComment( - application.name, - "running", - previewDomain, - ); - await updateIssueComment({ - ...issueParams, - body: `### Dokploy Preview Deployment\n\n${buildingComment}`, - }); - application.appName = previewDeployment.appName; - application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`; - application.buildArgs = `${application.previewBuildArgs}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`; - application.buildSecrets = `${application.previewBuildSecrets}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`; - - if (application.serverId) { - let command = "set -e;"; - if (application.sourceType === "github") { - command += await getGithubCloneCommand({ - ...application, - appName: previewDeployment.appName, - branch: previewDeployment.branch, - serverId: application.serverId, - logPath: deployment.logPath, - }); - } - - command += getBuildCommand(application, deployment.logPath); - await execAsyncRemote(application.serverId, command); - await mechanizeDockerContainer(application); - } - - const successComment = getIssueComment( - application.name, - "success", - previewDomain, - ); - await updateIssueComment({ - ...issueParams, - body: `### Dokploy Preview Deployment\n\n${successComment}`, - }); - await updateDeploymentStatus(deployment.deploymentId, "done"); - await updatePreviewDeployment(previewDeploymentId, { - previewStatus: "done", - }); - } catch (error) { - const comment = getIssueComment(application.name, "error", previewDomain); - await updateIssueComment({ - ...issueParams, - body: `### Dokploy Preview Deployment\n\n${comment}`, - }); - await updateDeploymentStatus(deployment.deploymentId, "error"); - await updatePreviewDeployment(previewDeploymentId, { - previewStatus: "error", - }); - throw error; - } - - return true; -}; - -export const rebuildRemoteApplication = async ({ - applicationId, - titleLog = "Rebuild deployment", - descriptionLog = "", -}: { - applicationId: string; - titleLog: string; - descriptionLog: string; -}) => { - const application = await findApplicationById(applicationId); - - const deployment = await createDeployment({ - applicationId: applicationId, - title: titleLog, - description: descriptionLog, - }); - - try { - if (application.serverId) { - if (application.sourceType !== "docker") { - let command = "set -e;"; - command += getBuildCommand(application, deployment.logPath); - await execAsyncRemote(application.serverId, command); - } - await mechanizeDockerContainer(application); - } - await updateDeploymentStatus(deployment.deploymentId, "done"); - await updateApplicationStatus(applicationId, "done"); - } catch (error) { - // @ts-ignore - const encodedContent = encodeBase64(error?.message); - - await execAsyncRemote( - application.serverId, - ` - echo "\n\n===================================EXTRA LOGS============================================" >> ${deployment.logPath}; - echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath}; - echo "${encodedContent}" | base64 -d >> "${deployment.logPath}";`, - ); - await updateDeploymentStatus(deployment.deploymentId, "error"); - await updateApplicationStatus(applicationId, "error"); - throw error; - } - - return true; -}; - export const getApplicationStats = async (appName: string) => { + if (appName === "dokploy") { + return await getAdvancedStats(appName); + } const filter = { status: ["running"], label: [`com.docker.swarm.service.name=${appName}`], diff --git a/packages/server/src/services/compose.ts b/packages/server/src/services/compose.ts index 1436c52cc..89a12a156 100644 --- a/packages/server/src/services/compose.ts +++ b/packages/server/src/services/compose.ts @@ -7,14 +7,10 @@ import { cleanAppName, compose, } from "@dokploy/server/db/schema"; -import { - buildCompose, - getBuildComposeCommand, -} from "@dokploy/server/utils/builders/compose"; +import { getBuildComposeCommand } from "@dokploy/server/utils/builders/compose"; import { randomizeSpecificationFile } from "@dokploy/server/utils/docker/compose"; import { cloneCompose, - cloneComposeRemote, loadDockerCompose, loadDockerComposeRemote, } from "@dokploy/server/utils/docker/domain"; @@ -22,38 +18,28 @@ import type { ComposeSpecification } from "@dokploy/server/utils/docker/types"; import { sendBuildErrorNotifications } from "@dokploy/server/utils/notifications/build-error"; import { sendBuildSuccessNotifications } from "@dokploy/server/utils/notifications/build-success"; import { + ExecError, execAsync, execAsyncRemote, } from "@dokploy/server/utils/process/execAsync"; -import { - cloneBitbucketRepository, - getBitbucketCloneCommand, -} from "@dokploy/server/utils/providers/bitbucket"; +import { cloneBitbucketRepository } from "@dokploy/server/utils/providers/bitbucket"; import { cloneGitRepository, - getCustomGitCloneCommand, + getGitCommitInfo, } from "@dokploy/server/utils/providers/git"; -import { - cloneGiteaRepository, - getGiteaCloneCommand, -} from "@dokploy/server/utils/providers/gitea"; -import { - cloneGithubRepository, - getGithubCloneCommand, -} from "@dokploy/server/utils/providers/github"; -import { - cloneGitlabRepository, - getGitlabCloneCommand, -} from "@dokploy/server/utils/providers/gitlab"; -import { - createComposeFile, - getCreateComposeFileCommand, -} from "@dokploy/server/utils/providers/raw"; +import { cloneGiteaRepository } from "@dokploy/server/utils/providers/gitea"; +import { cloneGithubRepository } from "@dokploy/server/utils/providers/github"; +import { cloneGitlabRepository } from "@dokploy/server/utils/providers/gitlab"; +import { getCreateComposeFileCommand } from "@dokploy/server/utils/providers/raw"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; import { encodeBase64 } from "../utils/docker/utils"; import { getDokployUrl } from "./admin"; -import { createDeploymentCompose, updateDeploymentStatus } from "./deployment"; +import { + createDeploymentCompose, + updateDeployment, + updateDeploymentStatus, +} from "./deployment"; import { validUniqueServerAppName } from "./project"; export type Compose = typeof compose.$inferSelect; @@ -163,10 +149,11 @@ export const loadServices = async ( const compose = await findComposeById(composeId); if (type === "fetch") { + const command = await cloneCompose(compose); if (compose.serverId) { - await cloneComposeRemote(compose); + await execAsyncRemote(compose.serverId, command); } else { - await cloneCompose(compose); + await execAsync(command); } } @@ -235,24 +222,41 @@ export const deployCompose = async ({ }); try { + const entity = { + ...compose, + type: "compose" as const, + }; + let command = "set -e;"; if (compose.sourceType === "github") { - await cloneGithubRepository({ - ...compose, - logPath: deployment.logPath, - type: "compose", - }); + command += await cloneGithubRepository(entity); } else if (compose.sourceType === "gitlab") { - await cloneGitlabRepository(compose, deployment.logPath, true); + command += await cloneGitlabRepository(entity); } else if (compose.sourceType === "bitbucket") { - await cloneBitbucketRepository(compose, deployment.logPath, true); + command += await cloneBitbucketRepository(entity); } else if (compose.sourceType === "git") { - await cloneGitRepository(compose, deployment.logPath, true); + command += await cloneGitRepository(entity); } else if (compose.sourceType === "gitea") { - await cloneGiteaRepository(compose, deployment.logPath, true); + command += await cloneGiteaRepository(entity); } else if (compose.sourceType === "raw") { - await createComposeFile(compose, deployment.logPath); + command += getCreateComposeFileCommand(entity); } - await buildCompose(compose, deployment.logPath); + + let commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`; + if (compose.serverId) { + await execAsyncRemote(compose.serverId, commandWithLog); + } else { + await execAsync(commandWithLog); + } + + command = "set -e;"; + command += await getBuildComposeCommand(entity); + commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`; + if (compose.serverId) { + await execAsyncRemote(compose.serverId, commandWithLog); + } else { + await execAsync(commandWithLog); + } + await updateDeploymentStatus(deployment.deploymentId, "done"); await updateCompose(composeId, { composeStatus: "done", @@ -265,8 +269,24 @@ export const deployCompose = async ({ buildLink, organizationId: compose.environment.project.organizationId, domains: compose.domains, + environmentName: compose.environment.name, }); } 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 updateCompose(composeId, { composeStatus: "error", @@ -281,6 +301,19 @@ export const deployCompose = async ({ organizationId: compose.environment.project.organizationId, }); throw error; + } finally { + if (compose.sourceType !== "raw") { + const commitInfo = await getGitCommitInfo({ + ...compose, + type: "compose", + }); + if (commitInfo) { + await updateDeployment(deployment.deploymentId, { + title: commitInfo.message, + description: `Commit: ${commitInfo.hash}`, + }); + } + } } }; @@ -302,154 +335,23 @@ export const rebuildCompose = async ({ }); try { + let command = "set -e;"; if (compose.sourceType === "raw") { - await createComposeFile(compose, deployment.logPath); + command += getCreateComposeFileCommand(compose); } - await buildCompose(compose, deployment.logPath); - await updateDeploymentStatus(deployment.deploymentId, "done"); - await updateCompose(composeId, { - composeStatus: "done", - }); - } catch (error) { - await updateDeploymentStatus(deployment.deploymentId, "error"); - await updateCompose(composeId, { - composeStatus: "error", - }); - throw error; - } - - return true; -}; - -export const deployRemoteCompose = async ({ - composeId, - titleLog = "Manual deployment", - descriptionLog = "", -}: { - composeId: string; - titleLog: string; - descriptionLog: string; -}) => { - const compose = await findComposeById(composeId); - - const buildLink = `${await getDokployUrl()}/dashboard/project/${ - compose.environment.projectId - }/environment/${compose.environmentId}/services/compose/${compose.composeId}?tab=deployments`; - const deployment = await createDeploymentCompose({ - composeId: composeId, - title: titleLog, - description: descriptionLog, - }); - try { + let commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`; if (compose.serverId) { - let command = "set -e;"; - - if (compose.sourceType === "github") { - command += await getGithubCloneCommand({ - ...compose, - logPath: deployment.logPath, - type: "compose", - serverId: compose.serverId, - }); - } else if (compose.sourceType === "gitlab") { - command += await getGitlabCloneCommand( - compose, - deployment.logPath, - true, - ); - } else if (compose.sourceType === "bitbucket") { - command += await getBitbucketCloneCommand( - compose, - deployment.logPath, - true, - ); - } else if (compose.sourceType === "git") { - command += await getCustomGitCloneCommand( - compose, - deployment.logPath, - true, - ); - console.log(command); - } else if (compose.sourceType === "raw") { - command += getCreateComposeFileCommand(compose, deployment.logPath); - } else if (compose.sourceType === "gitea") { - command += await getGiteaCloneCommand( - compose, - deployment.logPath, - true, - ); - } - - await execAsyncRemote(compose.serverId, command); - await getBuildComposeCommand(compose, deployment.logPath); - } - - await updateDeploymentStatus(deployment.deploymentId, "done"); - await updateCompose(composeId, { - composeStatus: "done", - }); - - await sendBuildSuccessNotifications({ - projectName: compose.environment.project.name, - applicationName: compose.name, - applicationType: "compose", - buildLink, - organizationId: compose.environment.project.organizationId, - domains: compose.domains, - }); - } catch (error) { - // @ts-ignore - const encodedContent = encodeBase64(error?.message); - - await execAsyncRemote( - compose.serverId, - ` - echo "\n\n===================================EXTRA LOGS============================================" >> ${deployment.logPath}; - echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath}; - echo "${encodedContent}" | base64 -d >> "${deployment.logPath}";`, - ); - await updateDeploymentStatus(deployment.deploymentId, "error"); - await updateCompose(composeId, { - composeStatus: "error", - }); - await sendBuildErrorNotifications({ - projectName: compose.environment.project.name, - applicationName: compose.name, - applicationType: "compose", - // @ts-ignore - errorMessage: error?.message || "Error building", - buildLink, - organizationId: compose.environment.project.organizationId, - }); - throw error; - } -}; - -export const rebuildRemoteCompose = async ({ - composeId, - titleLog = "Rebuild deployment", - descriptionLog = "", -}: { - composeId: string; - titleLog: string; - descriptionLog: string; -}) => { - const compose = await findComposeById(composeId); - - const deployment = await createDeploymentCompose({ - composeId: composeId, - title: titleLog, - description: descriptionLog, - }); - - try { - if (compose.sourceType === "raw") { - const command = getCreateComposeFileCommand(compose, deployment.logPath); - await execAsyncRemote(compose.serverId, command); + await execAsyncRemote(compose.serverId, commandWithLog); + } else { + await execAsync(commandWithLog); } + command += await getBuildComposeCommand(compose); + commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`; if (compose.serverId) { - await getBuildComposeCommand(compose, deployment.logPath); + await execAsyncRemote(compose.serverId, commandWithLog); + } else { + await execAsync(commandWithLog); } await updateDeploymentStatus(deployment.deploymentId, "done"); @@ -457,16 +359,21 @@ export const rebuildRemoteCompose = async ({ composeStatus: "done", }); } catch (error) { - // @ts-ignore - const encodedContent = encodeBase64(error?.message); + let command = ""; - await execAsyncRemote( - compose.serverId, - ` - echo "\n\n===================================EXTRA LOGS============================================" >> ${deployment.logPath}; - echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath}; - echo "${encodedContent}" | base64 -d >> "${deployment.logPath}";`, - ); + // 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 updateCompose(composeId, { composeStatus: "error", @@ -501,7 +408,7 @@ export const removeCompose = async ( } else { const command = ` 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" : "" } && rm -rf ${projectPath}`; @@ -528,7 +435,7 @@ export const startCompose = async (composeId: string) => { const projectPath = join(COMPOSE_PATH, compose.appName, "code"); const path = 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.serverId) { await execAsyncRemote( @@ -563,14 +470,17 @@ export const stopCompose = async (composeId: string) => { if (compose.serverId) { await execAsyncRemote( 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 } stop`, ); } else { - await execAsync(`docker compose -p ${compose.appName} stop`, { - cwd: join(COMPOSE_PATH, compose.appName), - }); + await execAsync( + `env -i PATH="$PATH" docker compose -p ${compose.appName} stop`, + { + cwd: join(COMPOSE_PATH, compose.appName), + }, + ); } } diff --git a/packages/server/src/services/deployment.ts b/packages/server/src/services/deployment.ts index ed03b32fc..41274bac9 100644 --- a/packages/server/src/services/deployment.ts +++ b/packages/server/src/services/deployment.ts @@ -74,20 +74,21 @@ export const createDeployment = async ( >, ) => { const application = await findApplicationById(deployment.applicationId); - try { await removeLastTenDeployments( deployment.applicationId, "application", application.serverId, ); - const { LOGS_PATH } = paths(!!application.serverId); + const serverId = application.serverId; + + const { LOGS_PATH } = paths(!!serverId); const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss"); const fileName = `${application.appName}-${formattedDateTime}.log`; const logFilePath = path.join(LOGS_PATH, application.appName, fileName); - if (application.serverId) { - const server = await findServerById(application.serverId); + if (serverId) { + const server = await findServerById(serverId); const command = ` mkdir -p ${LOGS_PATH}/${application.appName}; @@ -99,7 +100,7 @@ export const createDeployment = async ( await fsPromises.mkdir(path.join(LOGS_PATH, application.appName), { recursive: true, }); - await fsPromises.writeFile(logFilePath, "Initializing deployment"); + await fsPromises.writeFile(logFilePath, "Initializing deployment\n"); } const deploymentCreate = await db @@ -249,7 +250,7 @@ export const createDeploymentCompose = async ( const command = ` mkdir -p ${LOGS_PATH}/${compose.appName}; -echo "Initializing deployment" >> ${logFilePath}; +echo "Initializing deployment\n" >> ${logFilePath}; `; await execAsyncRemote(server.serverId, command); @@ -257,7 +258,7 @@ echo "Initializing deployment" >> ${logFilePath}; await fsPromises.mkdir(path.join(LOGS_PATH, compose.appName), { recursive: true, }); - await fsPromises.writeFile(logFilePath, "Initializing deployment"); + await fsPromises.writeFile(logFilePath, "Initializing deployment\n"); } const deploymentCreate = await db diff --git a/packages/server/src/services/environment.ts b/packages/server/src/services/environment.ts index 1d77510be..fb1952818 100644 --- a/packages/server/src/services/environment.ts +++ b/packages/server/src/services/environment.ts @@ -34,13 +34,43 @@ export const findEnvironmentById = async (environmentId: string) => { const environment = await db.query.environments.findFirst({ where: eq(environments.environmentId, environmentId), with: { - applications: true, - mariadb: true, - mongo: true, - mysql: true, - postgres: true, - redis: true, - compose: true, + applications: { + with: { + deployments: true, + server: true, + }, + }, + mariadb: { + with: { + server: true, + }, + }, + mongo: { + with: { + server: true, + }, + }, + mysql: { + with: { + server: true, + }, + }, + postgres: { + with: { + server: true, + }, + }, + redis: { + with: { + server: true, + }, + }, + compose: { + with: { + deployments: true, + server: true, + }, + }, project: true, }, }); diff --git a/packages/server/src/services/postgres.ts b/packages/server/src/services/postgres.ts index 0d900443e..d7016038a 100644 --- a/packages/server/src/services/postgres.ts +++ b/packages/server/src/services/postgres.ts @@ -13,6 +13,18 @@ import { TRPCError } from "@trpc/server"; import { eq, getTableColumns } from "drizzle-orm"; 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 const createPostgres = async (input: typeof apiCreatePostgres._type) => { diff --git a/packages/server/src/services/settings.ts b/packages/server/src/services/settings.ts index 301573cb4..23d11b09b 100644 --- a/packages/server/src/services/settings.ts +++ b/packages/server/src/services/settings.ts @@ -59,10 +59,8 @@ export const getUpdateData = async (): Promise => { let currentDigest: string; try { currentDigest = await getServiceImageDigest(); - } catch { - // 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 + } catch (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. return DEFAULT_UPDATE_DATA; } diff --git a/packages/server/src/setup/postgres-setup.ts b/packages/server/src/setup/postgres-setup.ts index cf162f1ed..377a84952 100644 --- a/packages/server/src/setup/postgres-setup.ts +++ b/packages/server/src/setup/postgres-setup.ts @@ -17,7 +17,7 @@ export const initializePostgres = async () => { Mounts: [ { Type: "volume", - Source: "dokploy-postgres-database", + Source: "dokploy-postgres", Target: "/var/lib/postgresql/data", }, ], diff --git a/packages/server/src/setup/redis-setup.ts b/packages/server/src/setup/redis-setup.ts index 7366546da..894b3427d 100644 --- a/packages/server/src/setup/redis-setup.ts +++ b/packages/server/src/setup/redis-setup.ts @@ -14,7 +14,7 @@ export const initializeRedis = async () => { Mounts: [ { Type: "volume", - Source: "redis-data-volume", + Source: "dokploy-redis", Target: "/data", }, ], diff --git a/packages/server/src/setup/traefik-setup.ts b/packages/server/src/setup/traefik-setup.ts index fa9bf78d0..73cff0b1c 100644 --- a/packages/server/src/setup/traefik-setup.ts +++ b/packages/server/src/setup/traefik-setup.ts @@ -20,7 +20,7 @@ export const TRAEFIK_PORT = Number.parseInt(process.env.TRAEFIK_PORT!, 10) || 80; export const TRAEFIK_HTTP3_PORT = Number.parseInt(process.env.TRAEFIK_HTTP3_PORT!, 10) || 443; -export const TRAEFIK_VERSION = process.env.TRAEFIK_VERSION || "3.5.0"; +export const TRAEFIK_VERSION = process.env.TRAEFIK_VERSION || "3.6.1"; export interface TraefikOptions { env?: string[]; diff --git a/packages/server/src/utils/builders/compose.ts b/packages/server/src/utils/builders/compose.ts index 667b46b74..fe5417ea5 100644 --- a/packages/server/src/utils/builders/compose.ts +++ b/packages/server/src/utils/builders/compose.ts @@ -1,117 +1,29 @@ -import { - createWriteStream, - existsSync, - mkdirSync, - writeFileSync, -} from "node:fs"; import { dirname, join } from "node:path"; import { paths } from "@dokploy/server/constants"; import type { InferResultType } from "@dokploy/server/types/with"; import boxen from "boxen"; -import { - writeDomainsToCompose, - writeDomainsToComposeRemote, -} from "../docker/domain"; +import { quote } from "shell-quote"; +import { writeDomainsToCompose } from "../docker/domain"; import { encodeBase64, getEnviromentVariablesObject, prepareEnvironmentVariables, } from "../docker/utils"; -import { execAsync, execAsyncRemote } from "../process/execAsync"; -import { spawnAsync } from "../process/spawnAsync"; export type ComposeNested = InferResultType< "compose", { environment: { with: { project: true } }; mounts: true; domains: true } >; -export const buildCompose = async (compose: ComposeNested, logPath: string) => { - const writeStream = createWriteStream(logPath, { flags: "a" }); - const { sourceType, appName, mounts, composeType, domains } = compose; - try { - const { COMPOSE_PATH } = paths(); - const command = createCommand(compose); - await writeDomainsToCompose(compose, domains); - createEnvFile(compose); - if (compose.isolatedDeployment) { - await execAsync( - `docker network inspect ${compose.appName} >/dev/null 2>&1 || docker network create ${composeType === "stack" ? "--driver overlay" : ""} --attachable ${compose.appName}`, - ); - } - - const logContent = ` - App Name: ${appName} - Build Compose 🐳 - Detected: ${mounts.length} mounts πŸ“‚ - Command: docker ${command} - Source Type: docker ${sourceType} βœ… - Compose Type: ${composeType} βœ…`; - const logBox = boxen(logContent, { - padding: { - left: 1, - right: 1, - bottom: 1, - }, - width: 80, - borderStyle: "double", - }); - writeStream.write(`\n${logBox}\n`); - const projectPath = join(COMPOSE_PATH, compose.appName, "code"); - - await spawnAsync( - "docker", - [...command.split(" ")], - (data) => { - if (writeStream.writable) { - writeStream.write(data.toString()); - } - }, - { - cwd: projectPath, - env: { - NODE_ENV: process.env.NODE_ENV, - PATH: process.env.PATH, - ...(composeType === "stack" && { - ...getEnviromentVariablesObject( - compose.env, - compose.environment.project.env, - ), - }), - }, - }, - ); - - if (compose.isolatedDeployment) { - await execAsync( - `docker network connect ${compose.appName} $(docker ps --filter "name=dokploy-traefik" -q) >/dev/null 2>&1`, - ).catch(() => {}); - } - - writeStream.write("Docker Compose Deployed: βœ…"); - } catch (error) { - writeStream.write(`Error ❌ ${(error as Error).message}`); - throw error; - } finally { - writeStream.end(); - } -}; - -export const getBuildComposeCommand = async ( - compose: ComposeNested, - logPath: string, -) => { - const { COMPOSE_PATH } = paths(true); +export const getBuildComposeCommand = async (compose: ComposeNested) => { + const { COMPOSE_PATH } = paths(!!compose.serverId); const { sourceType, appName, mounts, composeType, domains } = compose; const command = createCommand(compose); const envCommand = getCreateEnvFileCommand(compose); const projectPath = join(COMPOSE_PATH, compose.appName, "code"); const exportEnvCommand = getExportEnvCommand(compose); - const newCompose = await writeDomainsToComposeRemote( - compose, - domains, - logPath, - ); + const newCompose = await writeDomainsToCompose(compose, domains); const logContent = ` App Name: ${appName} Build Compose 🐳 @@ -133,7 +45,7 @@ Compose Type: ${composeType} βœ…`; const bashCommand = ` set -e { - echo "${logBox}" >> "${logPath}" + echo "${logBox}"; ${newCompose} @@ -141,19 +53,18 @@ Compose Type: ${composeType} βœ…`; cd "${projectPath}"; - ${exportEnvCommand} ${compose.isolatedDeployment ? `docker network inspect ${compose.appName} >/dev/null 2>&1 || docker network create --attachable ${compose.appName}` : ""} - docker ${command.split(" ").join(" ")} >> "${logPath}" 2>&1 || { echo "Error: ❌ Docker command failed" >> "${logPath}"; 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` : ""} - echo "Docker Compose Deployed: βœ…" >> "${logPath}" + echo "Docker Compose Deployed: βœ…"; } || { - echo "Error: ❌ Script execution failed" >> "${logPath}" + echo "Error: ❌ Script execution failed"; exit 1 } `; - return await execAsyncRemote(compose.serverId, bashCommand); + return bashCommand; }; const sanitizeCommand = (command: string) => { @@ -185,38 +96,8 @@ export const createCommand = (compose: ComposeNested) => { return command; }; -const createEnvFile = (compose: ComposeNested) => { - const { COMPOSE_PATH } = paths(); - const { env, composePath, appName } = compose; - const composeFilePath = - join(COMPOSE_PATH, appName, "code", composePath) || - join(COMPOSE_PATH, appName, "code", "docker-compose.yml"); - - const envFilePath = join(dirname(composeFilePath), ".env"); - let envContent = `APP_NAME=${appName}\n`; - envContent += env || ""; - if (!envContent.includes("DOCKER_CONFIG")) { - envContent += "\nDOCKER_CONFIG=/root/.docker"; - } - - if (compose.randomize) { - envContent += `\nCOMPOSE_PREFIX=${compose.suffix}`; - } - - const envFileContent = prepareEnvironmentVariables( - envContent, - compose.environment.project.env, - compose.environment.env, - ).join("\n"); - - if (!existsSync(dirname(envFilePath))) { - mkdirSync(dirname(envFilePath), { recursive: true }); - } - writeFileSync(envFilePath, envFileContent); -}; - export const getCreateEnvFileCommand = (compose: ComposeNested) => { - const { COMPOSE_PATH } = paths(true); + const { COMPOSE_PATH } = paths(!!compose.serverId); const { env, composePath, appName } = compose; const composeFilePath = join(COMPOSE_PATH, appName, "code", composePath) || @@ -255,8 +136,8 @@ const getExportEnvCommand = (compose: ComposeNested) => { compose.environment.project.env, ); const exports = Object.entries(envVars) - .map(([key, value]) => `export ${key}=${JSON.stringify(value)}`) - .join("\n"); + .map(([key, value]) => `${key}=${quote([value])}`) + .join(" "); - return exports ? `\n# Export environment variables\n${exports}\n` : ""; + return exports ? `${exports}` : ""; }; diff --git a/packages/server/src/utils/builders/docker-file.ts b/packages/server/src/utils/builders/docker-file.ts index b5c2b59c3..8ca99ccf2 100644 --- a/packages/server/src/utils/builders/docker-file.ts +++ b/packages/server/src/utils/builders/docker-file.ts @@ -1,109 +1,16 @@ -import type { WriteStream } from "node:fs"; import { getEnviromentVariablesObject, - prepareEnvironmentVariables, + prepareEnvironmentVariablesForShell, } from "@dokploy/server/utils/docker/utils"; +import { quote } from "shell-quote"; import { getBuildAppDirectory, getDockerContextPath, } from "../filesystem/directory"; -import { spawnAsync } from "../process/spawnAsync"; import type { ApplicationNested } from "."; -import { createEnvFile, createEnvFileCommand } from "./utils"; +import { createEnvFileCommand } from "./utils"; -export const buildCustomDocker = async ( - application: ApplicationNested, - writeStream: WriteStream, -) => { - const { - appName, - env, - publishDirectory, - buildArgs, - buildSecrets, - dockerBuildStage, - cleanCache, - } = application; - const dockerFilePath = getBuildAppDirectory(application); - try { - const image = `${appName}`; - - const defaultContextPath = - dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || "."; - - const dockerContextPath = getDockerContextPath(application); - - const commandArgs = ["build", "-t", image, "-f", dockerFilePath, "."]; - - if (cleanCache) { - commandArgs.push("--no-cache"); - } - - if (dockerBuildStage) { - commandArgs.push("--target", dockerBuildStage); - } - - const args = prepareEnvironmentVariables( - buildArgs, - application.environment.project.env, - application.environment.env, - ); - - for (const arg of args) { - commandArgs.push("--build-arg", arg); - } - - const secrets = getEnviromentVariablesObject( - buildSecrets, - application.environment.project.env, - application.environment.env, - ); - - for (const key in secrets) { - // Although buildx is smart enough to know we may be referring to an environment variable name, - // we still make sure it doesn't fall back to type=file. - // See: https://docs.docker.com/reference/cli/docker/buildx/build/#secret - commandArgs.push("--secret", `type=env,id=${key}`); - } - - /* - Do not generate an environment file when publishDirectory is specified, - as it could be publicly exposed. - */ - if (!publishDirectory) { - createEnvFile( - dockerFilePath, - env, - application.environment.project.env, - application.environment.env, - ); - } - - await spawnAsync( - "docker", - commandArgs, - (data) => { - if (writeStream.writable) { - writeStream.write(data); - } - }, - { - cwd: dockerContextPath || defaultContextPath, - env: { - ...process.env, - ...secrets, - }, - }, - ); - } catch (error) { - throw error; - } -}; - -export const getDockerCommand = ( - application: ApplicationNested, - logPath: string, -) => { +export const getDockerCommand = (application: ApplicationNested) => { const { appName, env, @@ -134,14 +41,14 @@ export const getDockerCommand = ( commandArgs.push("--no-cache"); } - const args = prepareEnvironmentVariables( + const args = prepareEnvironmentVariablesForShell( buildArgs, application.environment.project.env, application.environment.env, ); for (const arg of args) { - commandArgs.push("--build-arg", `'${arg}'`); + commandArgs.push("--build-arg", arg); } const secrets = getEnviromentVariablesObject( @@ -151,7 +58,7 @@ export const getDockerCommand = ( ); const joinedSecrets = Object.entries(secrets) - .map(([key, value]) => `${key}='${value.replace(/'/g, "'\"'\"'")}'`) + .map(([key, value]) => `${key}=${quote([value])}`) .join(" "); for (const key in secrets) { @@ -176,17 +83,17 @@ export const getDockerCommand = ( } command += ` -echo "Building ${appName}" >> ${logPath}; -cd ${dockerContextPath} >> ${logPath} 2>> ${logPath} || { - echo "❌ The path ${dockerContextPath} does not exist" >> ${logPath}; +echo "Building ${appName}" ; +cd ${dockerContextPath} || { + echo "❌ The path ${dockerContextPath} does not exist" ; exit 1; } -${joinedSecrets} docker ${commandArgs.join(" ")} >> ${logPath} 2>> ${logPath} || { - echo "❌ Docker build failed" >> ${logPath}; +${joinedSecrets} docker ${commandArgs.join(" ")} || { + echo "❌ Docker build failed" ; exit 1; } -echo "βœ… Docker build completed." >> ${logPath}; +echo "βœ… Docker build completed." ; `; return command; diff --git a/packages/server/src/utils/builders/heroku.ts b/packages/server/src/utils/builders/heroku.ts index 3306f2fc2..8b38c694d 100644 --- a/packages/server/src/utils/builders/heroku.ts +++ b/packages/server/src/utils/builders/heroku.ts @@ -1,58 +1,12 @@ -import type { WriteStream } from "node:fs"; -import { prepareEnvironmentVariables } from "../docker/utils"; +import { prepareEnvironmentVariablesForShell } from "../docker/utils"; import { getBuildAppDirectory } from "../filesystem/directory"; -import { spawnAsync } from "../process/spawnAsync"; import type { ApplicationNested } from "."; -// TODO: integrate in the vps sudo chown -R $(whoami) ~/.docker -export const buildHeroku = async ( - application: ApplicationNested, - writeStream: WriteStream, -) => { - const { env, appName, cleanCache } = application; - const buildAppDirectory = getBuildAppDirectory(application); - const envVariables = prepareEnvironmentVariables( - env, - application.environment.project.env, - application.environment.env, - ); - try { - const args = [ - "build", - appName, - "--path", - buildAppDirectory, - "--builder", - `heroku/builder:${application.herokuVersion || "24"}`, - ]; - - for (const env of envVariables) { - args.push("--env", env); - } - - if (cleanCache) { - args.push("--clear-cache"); - } - - await spawnAsync("pack", args, (data) => { - if (writeStream.writable) { - writeStream.write(data); - } - }); - return true; - } catch (e) { - throw e; - } -}; - -export const getHerokuCommand = ( - application: ApplicationNested, - logPath: string, -) => { +export const getHerokuCommand = (application: ApplicationNested) => { const { env, appName, cleanCache } = application; const buildAppDirectory = getBuildAppDirectory(application); - const envVariables = prepareEnvironmentVariables( + const envVariables = prepareEnvironmentVariablesForShell( env, application.environment.project.env, application.environment.env, @@ -72,17 +26,17 @@ export const getHerokuCommand = ( } for (const env of envVariables) { - args.push("--env", `'${env}'`); + args.push("--env", env); } const command = `pack ${args.join(" ")}`; const bashCommand = ` -echo "Starting heroku build..." >> ${logPath}; -${command} >> ${logPath} 2>> ${logPath} || { - echo "❌ Heroku build failed" >> ${logPath}; +echo "Starting heroku build..." ; +${command} || { + echo "❌ Heroku build failed" ; exit 1; } -echo "βœ… Heroku build completed." >> ${logPath}; +echo "βœ… Heroku build completed." ; `; return bashCommand; diff --git a/packages/server/src/utils/builders/index.ts b/packages/server/src/utils/builders/index.ts index 5ae0704c5..bfc3e894a 100644 --- a/packages/server/src/utils/builders/index.ts +++ b/packages/server/src/utils/builders/index.ts @@ -1,7 +1,6 @@ -import { createWriteStream } from "node:fs"; import type { InferResultType } from "@dokploy/server/types/with"; import type { CreateServiceOptions } from "dockerode"; -import { uploadImage, uploadImageRemoteCommand } from "../cluster/upload"; +import { uploadImageRemoteCommand } from "../cluster/upload"; import { calculateResources, generateBindMounts, @@ -11,12 +10,12 @@ import { prepareEnvironmentVariables, } from "../docker/utils"; import { getRemoteDocker } from "../servers/remote-docker"; -import { buildCustomDocker, getDockerCommand } from "./docker-file"; -import { buildHeroku, getHerokuCommand } from "./heroku"; -import { buildNixpacks, getNixpacksCommand } from "./nixpacks"; -import { buildPaketo, getPaketoCommand } from "./paketo"; -import { buildRailpack, getRailpackCommand } from "./railpack"; -import { buildStatic, getStaticCommand } from "./static"; +import { getDockerCommand } from "./docker-file"; +import { getHerokuCommand } from "./heroku"; +import { getNixpacksCommand } from "./nixpacks"; +import { getPaketoCommand } from "./paketo"; +import { getRailpackCommand } from "./railpack"; +import { getStaticCommand } from "./static"; // NIXPACKS codeDirectory = where is the path of the code directory // HEROKU codeDirectory = where is the path of the code directory @@ -34,76 +33,35 @@ export type ApplicationNested = InferResultType< } >; -export const buildApplication = async ( - application: ApplicationNested, - logPath: string, -) => { - const writeStream = createWriteStream(logPath, { flags: "a" }); - const { buildType, sourceType } = application; - try { - writeStream.write( - `\nBuild ${buildType}: βœ…\nSource Type: ${sourceType}: βœ…\n`, - ); - console.log(`Build ${buildType}: βœ…`); - if (buildType === "nixpacks") { - await buildNixpacks(application, writeStream); - } else if (buildType === "heroku_buildpacks") { - await buildHeroku(application, writeStream); - } else if (buildType === "paketo_buildpacks") { - await buildPaketo(application, writeStream); - } else if (buildType === "dockerfile") { - await buildCustomDocker(application, writeStream); - } else if (buildType === "static") { - await buildStatic(application, writeStream); - } else if (buildType === "railpack") { - await buildRailpack(application, writeStream); - } - - if (application.registryId) { - await uploadImage(application, writeStream); - } - await mechanizeDockerContainer(application); - writeStream.write("Docker Deployed: βœ…"); - } catch (error) { - if (error instanceof Error) { - writeStream.write(`Error ❌\n${error?.message}`); - } else { - writeStream.write("Error ❌"); - } - throw error; - } finally { - writeStream.end(); - } -}; - -export const getBuildCommand = ( - application: ApplicationNested, - logPath: string, -) => { +export const getBuildCommand = (application: ApplicationNested) => { let command = ""; const { buildType, registry } = application; + + if (application.sourceType === "docker") { + return ""; + } switch (buildType) { case "nixpacks": - command = getNixpacksCommand(application, logPath); + command = getNixpacksCommand(application); break; case "heroku_buildpacks": - command = getHerokuCommand(application, logPath); + command = getHerokuCommand(application); break; case "paketo_buildpacks": - command = getPaketoCommand(application, logPath); + command = getPaketoCommand(application); break; case "static": - command = getStaticCommand(application, logPath); + command = getStaticCommand(application); break; case "dockerfile": - command = getDockerCommand(application, logPath); + command = getDockerCommand(application); break; case "railpack": - command = getRailpackCommand(application, logPath); + command = getRailpackCommand(application); break; } if (registry) { - command += uploadImageRemoteCommand(application, logPath); + command += uploadImageRemoteCommand(application); } return command; diff --git a/packages/server/src/utils/builders/nixpacks.ts b/packages/server/src/utils/builders/nixpacks.ts index 76905d0e7..b7134ea65 100644 --- a/packages/server/src/utils/builders/nixpacks.ts +++ b/packages/server/src/utils/builders/nixpacks.ts @@ -1,106 +1,16 @@ -import { existsSync, mkdirSync, type WriteStream } from "node:fs"; import path from "node:path"; -import { - buildStatic, - getStaticCommand, -} from "@dokploy/server/utils/builders/static"; +import { getStaticCommand } from "@dokploy/server/utils/builders/static"; import { nanoid } from "nanoid"; -import { prepareEnvironmentVariables } from "../docker/utils"; +import { prepareEnvironmentVariablesForShell } from "../docker/utils"; import { getBuildAppDirectory } from "../filesystem/directory"; -import { spawnAsync } from "../process/spawnAsync"; import type { ApplicationNested } from "."; -export const buildNixpacks = async ( - application: ApplicationNested, - writeStream: WriteStream, -) => { +export const getNixpacksCommand = (application: ApplicationNested) => { const { env, appName, publishDirectory, cleanCache } = application; const buildAppDirectory = getBuildAppDirectory(application); const buildContainerId = `${appName}-${nanoid(10)}`; - const envVariables = prepareEnvironmentVariables( - env, - application.environment.project.env, - application.environment.env, - ); - - const writeToStream = (data: string) => { - if (writeStream.writable) { - writeStream.write(data); - } - }; - - try { - const args = ["build", buildAppDirectory, "--name", appName]; - - if (cleanCache) { - args.push("--no-cache"); - } - - for (const env of envVariables) { - args.push("--env", env); - } - - if (publishDirectory) { - /* No need for any start command, since we'll use nginx later on */ - args.push("--no-error-without-start"); - } - - await spawnAsync("nixpacks", args, writeToStream); - - /* - Run the container with the image created by nixpacks, - and copy the artifacts on the host filesystem. - Then, remove the container and create a static build. - */ - if (publishDirectory) { - await spawnAsync( - "docker", - ["create", "--name", buildContainerId, appName], - writeToStream, - ); - - const localPath = path.join(buildAppDirectory, publishDirectory); - - if (!existsSync(path.dirname(localPath))) { - mkdirSync(path.dirname(localPath), { recursive: true }); - } - - // https://docs.docker.com/reference/cli/docker/container/cp/ - const isDirectory = - publishDirectory.endsWith("/") || !path.extname(publishDirectory); - - await spawnAsync( - "docker", - [ - "cp", - `${buildContainerId}:/app/${publishDirectory}${isDirectory ? "/." : ""}`, - localPath, - ], - writeToStream, - ); - - await spawnAsync("docker", ["rm", buildContainerId], writeToStream); - - await buildStatic(application, writeStream); - } - return true; - } catch (e) { - await spawnAsync("docker", ["rm", buildContainerId], writeToStream); - - throw e; - } -}; - -export const getNixpacksCommand = ( - application: ApplicationNested, - logPath: string, -) => { - const { env, appName, publishDirectory, cleanCache } = application; - - const buildAppDirectory = getBuildAppDirectory(application); - const buildContainerId = `${appName}-${nanoid(10)}`; - const envVariables = prepareEnvironmentVariables( + const envVariables = prepareEnvironmentVariablesForShell( env, application.environment.project.env, application.environment.env, @@ -113,7 +23,7 @@ export const getNixpacksCommand = ( } for (const env of envVariables) { - args.push("--env", `'${env}'`); + args.push("--env", env); } if (publishDirectory) { @@ -122,12 +32,12 @@ export const getNixpacksCommand = ( } const command = `nixpacks ${args.join(" ")}`; let bashCommand = ` -echo "Starting nixpacks build..." >> ${logPath}; -${command} >> ${logPath} 2>> ${logPath} || { - echo "❌ Nixpacks build failed" >> ${logPath}; - exit 1; -} -echo "βœ… Nixpacks build completed." >> ${logPath}; + echo "Starting nixpacks build..." ; + ${command} || { + echo "❌ Nixpacks build failed" ; + exit 1; + } + echo "βœ… Nixpacks build completed." ; `; /* @@ -141,16 +51,16 @@ echo "βœ… Nixpacks build completed." >> ${logPath}; publishDirectory.endsWith("/") || !path.extname(publishDirectory); bashCommand += ` -docker create --name ${buildContainerId} ${appName} -mkdir -p ${localPath} -docker cp ${buildContainerId}:/app/${publishDirectory}${isDirectory ? "/." : ""} ${path.join(buildAppDirectory, publishDirectory)} >> ${logPath} 2>> ${logPath} || { + docker create --name ${buildContainerId} ${appName} + mkdir -p ${localPath} + docker cp ${buildContainerId}:/app/${publishDirectory}${isDirectory ? "/." : ""} ${path.join(buildAppDirectory, publishDirectory)} || { + docker rm ${buildContainerId} + echo "❌ Copying ${publishDirectory} to ${path.join(buildAppDirectory, publishDirectory)} failed" ; + exit 1; + } docker rm ${buildContainerId} - echo "❌ Copying ${publishDirectory} to ${path.join(buildAppDirectory, publishDirectory)} failed" >> ${logPath}; - exit 1; -} -docker rm ${buildContainerId} -${getStaticCommand(application, logPath)} - `; + ${getStaticCommand(application)} + `; } return bashCommand; diff --git a/packages/server/src/utils/builders/paketo.ts b/packages/server/src/utils/builders/paketo.ts index b95a1bb31..bb4f8c8a4 100644 --- a/packages/server/src/utils/builders/paketo.ts +++ b/packages/server/src/utils/builders/paketo.ts @@ -1,57 +1,12 @@ -import type { WriteStream } from "node:fs"; -import { prepareEnvironmentVariables } from "../docker/utils"; +import { prepareEnvironmentVariablesForShell } from "../docker/utils"; import { getBuildAppDirectory } from "../filesystem/directory"; -import { spawnAsync } from "../process/spawnAsync"; import type { ApplicationNested } from "."; -export const buildPaketo = async ( - application: ApplicationNested, - writeStream: WriteStream, -) => { - const { env, appName, cleanCache } = application; - const buildAppDirectory = getBuildAppDirectory(application); - const envVariables = prepareEnvironmentVariables( - env, - application.environment.project.env, - application.environment.env, - ); - try { - const args = [ - "build", - appName, - "--path", - buildAppDirectory, - "--builder", - "paketobuildpacks/builder-jammy-full", - ]; - - if (cleanCache) { - args.push("--clear-cache"); - } - - for (const env of envVariables) { - args.push("--env", env); - } - - await spawnAsync("pack", args, (data) => { - if (writeStream.writable) { - writeStream.write(data); - } - }); - return true; - } catch (e) { - throw e; - } -}; - -export const getPaketoCommand = ( - application: ApplicationNested, - logPath: string, -) => { +export const getPaketoCommand = (application: ApplicationNested) => { const { env, appName, cleanCache } = application; const buildAppDirectory = getBuildAppDirectory(application); - const envVariables = prepareEnvironmentVariables( + const envVariables = prepareEnvironmentVariablesForShell( env, application.environment.project.env, application.environment.env, @@ -71,17 +26,17 @@ export const getPaketoCommand = ( } for (const env of envVariables) { - args.push("--env", `'${env}'`); + args.push("--env", env); } const command = `pack ${args.join(" ")}`; const bashCommand = ` -echo "Starting Paketo build..." >> ${logPath}; -${command} >> ${logPath} 2>> ${logPath} || { - echo "❌ Paketo build failed" >> ${logPath}; +echo "Starting Paketo build..." ; +${command} || { + echo "❌ Paketo build failed" ; exit 1; } -echo "βœ… Paketo build completed." >> ${logPath}; +echo "βœ… Paketo build completed." ; `; return bashCommand; diff --git a/packages/server/src/utils/builders/railpack.ts b/packages/server/src/utils/builders/railpack.ts index 4adc9ca1c..62fa9f975 100644 --- a/packages/server/src/utils/builders/railpack.ts +++ b/packages/server/src/utils/builders/railpack.ts @@ -1,13 +1,12 @@ import { createHash } from "node:crypto"; -import type { WriteStream } from "node:fs"; import { nanoid } from "nanoid"; +import { quote } from "shell-quote"; import { parseEnvironmentKeyValuePair, prepareEnvironmentVariables, + prepareEnvironmentVariablesForShell, } from "../docker/utils"; import { getBuildAppDirectory } from "../filesystem/directory"; -import { execAsync } from "../process/execAsync"; -import { spawnAsync } from "../process/spawnAsync"; import type { ApplicationNested } from "."; const calculateSecretsHash = (envVariables: string[]): string => { @@ -18,111 +17,10 @@ const calculateSecretsHash = (envVariables: string[]): string => { return hash.digest("hex"); }; -export const buildRailpack = async ( - application: ApplicationNested, - writeStream: WriteStream, -) => { +export const getRailpackCommand = (application: ApplicationNested) => { const { env, appName, cleanCache } = application; const buildAppDirectory = getBuildAppDirectory(application); - const envVariables = prepareEnvironmentVariables( - env, - application.environment.project.env, - application.environment.env, - ); - - try { - await execAsync( - "docker buildx create --use --name builder-containerd --driver docker-container || true", - ); - - await execAsync("docker buildx use builder-containerd"); - - // First prepare the build plan and info - const prepareArgs = [ - "prepare", - buildAppDirectory, - "--plan-out", - `${buildAppDirectory}/railpack-plan.json`, - "--info-out", - `${buildAppDirectory}/railpack-info.json`, - ]; - - // Add environment variables to prepare command - for (const env of envVariables) { - prepareArgs.push("--env", env); - } - - // Run prepare command - await spawnAsync("railpack", prepareArgs, (data) => { - if (writeStream.writable) { - writeStream.write(data); - } - }); - - // Calculate secrets hash for layer invalidation - const secretsHash = calculateSecretsHash(envVariables); - - // Build with BuildKit using the Railpack frontend - const cacheKey = cleanCache ? nanoid(10) : undefined; - const buildArgs = [ - "buildx", - "build", - ...(cacheKey - ? [ - "--build-arg", - `secrets-hash=${secretsHash}`, - "--build-arg", - `cache-key=${cacheKey}`, - ] - : []), - "--build-arg", - `BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v${application.railpackVersion}`, - "-f", - `${buildAppDirectory}/railpack-plan.json`, - "--output", - `type=docker,name=${appName}`, - ]; - - // Add secrets properly formatted - const env: { [key: string]: string } = {}; - for (const pair of envVariables) { - const [key, value] = parseEnvironmentKeyValuePair(pair); - if (key && value) { - buildArgs.push("--secret", `id=${key},env=${key}`); - env[key] = value; - } - } - - buildArgs.push(buildAppDirectory); - - await spawnAsync( - "docker", - buildArgs, - (data) => { - if (writeStream.writable) { - writeStream.write(data); - } - }, - { - env: { ...process.env, ...env }, - }, - ); - - return true; - } catch (e) { - throw e; - } finally { - await execAsync("docker buildx rm builder-containerd"); - } -}; - -export const getRailpackCommand = ( - application: ApplicationNested, - logPath: string, -) => { - const { env, appName, cleanCache } = application; - const buildAppDirectory = getBuildAppDirectory(application); - const envVariables = prepareEnvironmentVariables( + const envVariables = prepareEnvironmentVariablesForShell( env, application.environment.project.env, application.environment.env, @@ -139,7 +37,7 @@ export const getRailpackCommand = ( ]; for (const env of envVariables) { - prepareArgs.push("--env", `'${env}'`); + prepareArgs.push("--env", env); } // Calculate secrets hash for layer invalidation @@ -167,37 +65,49 @@ export const getRailpackCommand = ( ]; // 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 = []; - for (const pair of envVariables) { + for (const pair of rawEnvVariables) { const [key, value] = parseEnvironmentKeyValuePair(pair); if (key && value) { buildArgs.push("--secret", `id=${key},env=${key}`); - exportEnvs.push(`export ${key}='${value}'`); + exportEnvs.push(`export ${key}=${quote([value])}`); } } buildArgs.push(buildAppDirectory); const bashCommand = ` + # 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 use builder-containerd -echo "Preparing Railpack build plan..." >> "${logPath}"; -railpack ${prepareArgs.join(" ")} >> ${logPath} 2>> ${logPath} || { - echo "❌ Railpack prepare failed" >> ${logPath}; +echo "Preparing Railpack build plan..." ; +railpack ${prepareArgs.join(" ")} || { + echo "❌ Railpack prepare failed" ; + docker buildx rm builder-containerd || true exit 1; } -echo "βœ… Railpack prepare completed." >> ${logPath}; +echo "βœ… Railpack prepare completed." ; -echo "Building with Railpack frontend..." >> "${logPath}"; +echo "Building with Railpack frontend..." ; # Export environment variables for secrets ${exportEnvs.join("\n")} -docker ${buildArgs.join(" ")} >> ${logPath} 2>> ${logPath} || { - echo "❌ Railpack build failed" >> ${logPath}; +docker ${buildArgs.join(" ")} || { + echo "❌ Railpack build failed" ; + docker buildx rm builder-containerd || true exit 1; } -echo "βœ… Railpack build completed." >> ${logPath}; +echo "βœ… Railpack build completed." ; docker buildx rm builder-containerd `; diff --git a/packages/server/src/utils/builders/static.ts b/packages/server/src/utils/builders/static.ts index e59faa711..99fa25285 100644 --- a/packages/server/src/utils/builders/static.ts +++ b/packages/server/src/utils/builders/static.ts @@ -1,9 +1,5 @@ -import type { WriteStream } from "node:fs"; -import { - buildCustomDocker, - getDockerCommand, -} from "@dokploy/server/utils/builders/docker-file"; -import { createFile, getCreateFileCommand } from "../docker/utils"; +import { getDockerCommand } from "@dokploy/server/utils/builders/docker-file"; +import { getCreateFileCommand } from "../docker/utils"; import { getBuildAppDirectory } from "../filesystem/directory"; import type { ApplicationNested } from "."; @@ -32,81 +28,40 @@ http { } `; -export const buildStatic = async ( - application: ApplicationNested, - writeStream: WriteStream, -) => { +export const getStaticCommand = (application: ApplicationNested) => { const { publishDirectory, isStaticSpa } = application; const buildAppDirectory = getBuildAppDirectory(application); - - try { - if (isStaticSpa) { - createFile(buildAppDirectory, "nginx.conf", nginxSpaConfig); - } - - createFile( + let command = ""; + if (isStaticSpa) { + command += getCreateFileCommand( buildAppDirectory, - ".dockerignore", - [".git", ".env", "Dockerfile", ".dockerignore"].join("\n"), + "nginx.conf", + nginxSpaConfig, ); - - createFile( - buildAppDirectory, - "Dockerfile", - [ - "FROM nginx:alpine", - "WORKDIR /usr/share/nginx/html/", - isStaticSpa ? "COPY nginx.conf /etc/nginx/nginx.conf" : "", - `COPY ${publishDirectory || "."} .`, - 'CMD ["nginx", "-g", "daemon off;"]', - ].join("\n"), - ); - - createFile( - buildAppDirectory, - ".dockerignore", - [".git", ".env", "Dockerfile", ".dockerignore"].join("\n"), - ); - - await buildCustomDocker( - { - ...application, - buildType: "dockerfile", - dockerfile: "Dockerfile", - }, - writeStream, - ); - - return true; - } catch (e) { - throw e; } -}; -export const getStaticCommand = ( - application: ApplicationNested, - logPath: string, -) => { - const { publishDirectory } = application; - const buildAppDirectory = getBuildAppDirectory(application); + command += getCreateFileCommand( + buildAppDirectory, + ".dockerignore", + [".git", ".env", "Dockerfile", ".dockerignore"].join("\n"), + ); - let command = getCreateFileCommand( + command += getCreateFileCommand( buildAppDirectory, "Dockerfile", [ "FROM nginx:alpine", "WORKDIR /usr/share/nginx/html/", + isStaticSpa ? "COPY nginx.conf /etc/nginx/nginx.conf" : "", `COPY ${publishDirectory || "."} .`, + 'CMD ["nginx", "-g", "daemon off;"]', ].join("\n"), ); - command += getDockerCommand( - { - ...application, - buildType: "dockerfile", - dockerfile: "Dockerfile", - }, - logPath, - ); + command += getDockerCommand({ + ...application, + buildType: "dockerfile", + dockerfile: "Dockerfile", + }); return command; }; diff --git a/packages/server/src/utils/cluster/upload.ts b/packages/server/src/utils/cluster/upload.ts index c13a2701c..33be287e1 100644 --- a/packages/server/src/utils/cluster/upload.ts +++ b/packages/server/src/utils/cluster/upload.ts @@ -1,11 +1,6 @@ -import type { WriteStream } from "node:fs"; import type { ApplicationNested } from "../builders"; -import { spawnAsync } from "../process/spawnAsync"; -export const uploadImage = async ( - application: ApplicationNested, - writeStream: WriteStream, -) => { +export const uploadImageRemoteCommand = (application: ApplicationNested) => { const registry = application.registry; if (!registry) { @@ -19,85 +14,28 @@ export const uploadImage = async ( const finalURL = registryUrl; // Build registry tag in correct format: registry.com/owner/image:tag - // For ghcr.io: ghcr.io/username/image:tag - // For docker.io: docker.io/username/image:tag const registryTag = imagePrefix ? `${registryUrl ? `${registryUrl}/` : ""}${imagePrefix}/${imageName}` : `${registryUrl ? `${registryUrl}/` : ""}${username}/${imageName}`; - try { - writeStream.write( - `πŸ“¦ [Enabled Registry] Uploading image to ${registry.registryType} | ${imageName} | ${finalURL} | ${registryTag}\n`, - ); - const loginCommand = spawnAsync( - "docker", - ["login", finalURL, "-u", registry.username, "--password-stdin"], - (data) => { - if (writeStream.writable) { - writeStream.write(data); - } - }, - ); - loginCommand.child?.stdin?.write(registry.password); - loginCommand.child?.stdin?.end(); - await loginCommand; - - await spawnAsync("docker", ["tag", imageName, registryTag], (data) => { - if (writeStream.writable) { - writeStream.write(data); - } - }); - - await spawnAsync("docker", ["push", registryTag], (data) => { - if (writeStream.writable) { - writeStream.write(data); - } - }); - } catch (error) { - console.log(error); - throw error; - } -}; - -export const uploadImageRemoteCommand = ( - application: ApplicationNested, - logPath: string, -) => { - const registry = application.registry; - - if (!registry) { - throw new Error("Registry not found"); - } - - const { registryUrl, imagePrefix, username } = registry; - const { appName } = application; - const imageName = `${appName}:latest`; - - const finalURL = registryUrl; - - // Build registry tag in correct format: registry.com/owner/image:tag - const registryTag = imagePrefix - ? `${registryUrl}/${imagePrefix}/${imageName}` - : `${registryUrl}/${username}/${imageName}`; - try { const command = ` - echo "πŸ“¦ [Enabled Registry] Uploading image to '${registry.registryType}' | '${registryTag}'" >> ${logPath}; - echo "${registry.password}" | docker login ${finalURL} -u ${registry.username} --password-stdin >> ${logPath} 2>> ${logPath} || { - echo "❌ DockerHub Failed" >> ${logPath}; + echo "πŸ“¦ [Enabled Registry] Uploading image to '${registry.registryType}' | '${registryTag}'" ; + echo "${registry.password}" | docker login ${finalURL} -u ${registry.username} --password-stdin || { + echo "❌ DockerHub Failed" ; exit 1; } - echo "βœ… Registry Login Success" >> ${logPath}; - docker tag ${imageName} ${registryTag} >> ${logPath} 2>> ${logPath} || { - echo "❌ Error tagging image" >> ${logPath}; + echo "βœ… Registry Login Success" ; + docker tag ${imageName} ${registryTag} || { + echo "❌ Error tagging image" ; exit 1; } - echo "βœ… Image Tagged" >> ${logPath}; - docker push ${registryTag} 2>> ${logPath} || { - echo "❌ Error pushing image" >> ${logPath}; + echo "βœ… Image Tagged" ; + docker push ${registryTag} || { + echo "❌ Error pushing image" ; exit 1; } - echo "βœ… Image Pushed" >> ${logPath}; + echo "βœ… Image Pushed" ; `; return command; } catch (error) { diff --git a/packages/server/src/utils/docker/collision.ts b/packages/server/src/utils/docker/collision.ts index 9752100ca..88d20d4d8 100644 --- a/packages/server/src/utils/docker/collision.ts +++ b/packages/server/src/utils/docker/collision.ts @@ -1,11 +1,11 @@ import { findComposeById } from "@dokploy/server/services/compose"; import { stringify } from "yaml"; +import { execAsync, execAsyncRemote } from "../process/execAsync"; import { addAppNameToAllServiceNames } from "./collision/root-network"; import { generateRandomHash } from "./compose"; import { addSuffixToAllVolumes } from "./compose/volume"; import { cloneCompose, - cloneComposeRemote, loadDockerCompose, loadDockerComposeRemote, } from "./domain"; @@ -31,10 +31,11 @@ export const randomizeIsolatedDeploymentComposeFile = async ( ) => { const compose = await findComposeById(composeId); + const command = await cloneCompose(compose); if (compose.serverId) { - await cloneComposeRemote(compose); + await execAsyncRemote(compose.serverId, command); } else { - await cloneCompose(compose); + await execAsync(command); } let composeData: ComposeSpecification | null; diff --git a/packages/server/src/utils/docker/domain.ts b/packages/server/src/utils/docker/domain.ts index 7a9521d1d..2272f364e 100644 --- a/packages/server/src/utils/docker/domain.ts +++ b/packages/server/src/utils/docker/domain.ts @@ -1,35 +1,16 @@ import fs, { existsSync, readFileSync } from "node:fs"; -import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { paths } from "@dokploy/server/constants"; import type { Compose } from "@dokploy/server/services/compose"; import type { Domain } from "@dokploy/server/services/domain"; import { parse, stringify } from "yaml"; import { execAsyncRemote } from "../process/execAsync"; -import { - cloneRawBitbucketRepository, - cloneRawBitbucketRepositoryRemote, -} from "../providers/bitbucket"; -import { - cloneGitRawRepository, - cloneRawGitRepositoryRemote, -} from "../providers/git"; -import { - cloneRawGiteaRepository, - cloneRawGiteaRepositoryRemote, -} from "../providers/gitea"; -import { - cloneRawGithubRepository, - cloneRawGithubRepositoryRemote, -} from "../providers/github"; -import { - cloneRawGitlabRepository, - cloneRawGitlabRepositoryRemote, -} from "../providers/gitlab"; -import { - createComposeFileRaw, - createComposeFileRawRemote, -} from "../providers/raw"; +import { cloneBitbucketRepository } from "../providers/bitbucket"; +import { cloneGitRepository } from "../providers/git"; +import { cloneGiteaRepository } from "../providers/gitea"; +import { cloneGithubRepository } from "../providers/github"; +import { cloneGitlabRepository } from "../providers/gitlab"; +import { getCreateComposeFileCommand } from "../providers/raw"; import { randomizeDeployableSpecificationFile } from "./collision"; import { randomizeSpecificationFile } from "./compose"; import type { @@ -40,35 +21,25 @@ import type { import { encodeBase64 } from "./utils"; export const cloneCompose = async (compose: Compose) => { + let command = "set -e;"; + const entity = { + ...compose, + type: "compose" as const, + }; if (compose.sourceType === "github") { - await cloneRawGithubRepository(compose); + command += await cloneGithubRepository(entity); } else if (compose.sourceType === "gitlab") { - await cloneRawGitlabRepository(compose); + command += await cloneGitlabRepository(entity); } else if (compose.sourceType === "bitbucket") { - await cloneRawBitbucketRepository(compose); + command += await cloneBitbucketRepository(entity); } else if (compose.sourceType === "git") { - await cloneGitRawRepository(compose); + command += await cloneGitRepository(entity); } else if (compose.sourceType === "gitea") { - await cloneRawGiteaRepository(compose); + command += await cloneGiteaRepository(entity); } else if (compose.sourceType === "raw") { - await createComposeFileRaw(compose); - } -}; - -export const cloneComposeRemote = async (compose: Compose) => { - if (compose.sourceType === "github") { - await cloneRawGithubRepositoryRemote(compose); - } else if (compose.sourceType === "gitlab") { - await cloneRawGitlabRepositoryRemote(compose); - } else if (compose.sourceType === "bitbucket") { - await cloneRawBitbucketRepositoryRemote(compose); - } else if (compose.sourceType === "git") { - await cloneRawGitRepositoryRemote(compose); - } else if (compose.sourceType === "gitea") { - await cloneRawGiteaRepositoryRemote(compose); - } else if (compose.sourceType === "raw") { - await createComposeFileRawRemote(compose); + command += getCreateComposeFileCommand(compose); } + return command; }; export const getComposePath = (compose: Compose) => { @@ -134,25 +105,6 @@ export const readComposeFile = async (compose: Compose) => { export const writeDomainsToCompose = async ( compose: Compose, domains: Domain[], -) => { - if (!domains.length) { - return; - } - const composeConverted = await addDomainToCompose(compose, domains); - - const path = getComposePath(compose); - const composeString = stringify(composeConverted, { lineWidth: 1000 }); - try { - await writeFile(path, composeString, "utf8"); - } catch (error) { - throw error; - } -}; - -export const writeDomainsToComposeRemote = async ( - compose: Compose, - domains: Domain[], - logPath: string, ) => { if (!domains.length) { return ""; @@ -164,23 +116,21 @@ export const writeDomainsToComposeRemote = async ( if (!composeConverted) { return ` -echo "❌ Error: Compose file not found" >> ${logPath}; +echo "❌ Error: Compose file not found"; exit 1; `; } - if (compose.serverId) { - const composeString = stringify(composeConverted, { lineWidth: 1000 }); - const encodedContent = encodeBase64(composeString); - return `echo "${encodedContent}" | base64 -d > "${path}";`; - } + + const composeString = stringify(composeConverted, { lineWidth: 1000 }); + const encodedContent = encodeBase64(composeString); + return `echo "${encodedContent}" | base64 -d > "${path}";`; } catch (error) { // @ts-ignore - return `echo "❌ Has occured an error: ${error?.message || error}" >> ${logPath}; + return `echo "❌ Has occurred an error: ${error?.message || error}"; exit 1; `; } }; -// (node:59875) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 SIGTERM listeners added to [process]. Use emitter.setMaxListeners() to increase limit export const addDomainToCompose = async ( compose: Compose, domains: Domain[], @@ -190,7 +140,7 @@ export const addDomainToCompose = async ( let result: ComposeSpecification | null; if (compose.serverId) { - result = await loadDockerComposeRemote(compose); // aca hay que ir al servidor e ir a traer el compose file al servidor + result = await loadDockerComposeRemote(compose); } else { result = await loadDockerCompose(compose); } diff --git a/packages/server/src/utils/docker/utils.ts b/packages/server/src/utils/docker/utils.ts index 6d00aa0df..4258cfbbe 100644 --- a/packages/server/src/utils/docker/utils.ts +++ b/packages/server/src/utils/docker/utils.ts @@ -5,6 +5,7 @@ import { docker, paths } from "@dokploy/server/constants"; import type { Compose } from "@dokploy/server/services/compose"; import type { ContainerInfo, ResourceRequirements } from "dockerode"; import { parse } from "dotenv"; +import { quote } from "shell-quote"; import type { ApplicationNested } from "../builders"; import type { MariadbNested } from "../databases/mariadb"; import type { MongoNested } from "../databases/mongo"; @@ -310,6 +311,21 @@ export const prepareEnvironmentVariables = ( 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 = ( pair: string, ): [string, string] => { diff --git a/packages/server/src/utils/notifications/build-error.ts b/packages/server/src/utils/notifications/build-error.ts index f1746a890..f05fa8134 100644 --- a/packages/server/src/utils/notifications/build-error.ts +++ b/packages/server/src/utils/notifications/build-error.ts @@ -54,301 +54,303 @@ export const sendBuildErrorNotifications = async ({ for (const notification of notificationList) { const { email, discord, telegram, slack, gotify, ntfy, custom, lark } = notification; - if (email) { - const template = await renderAsync( - BuildFailedEmail({ + try { + if (email) { + const template = await renderAsync( + BuildFailedEmail({ + projectName, + applicationName, + applicationType, + errorMessage: errorMessage, + buildLink, + date: date.toLocaleString(), + }), + ).catch(); + await sendEmailNotification( + email, + "Build failed for dokploy", + template, + ); + } + + if (discord) { + const decorate = (decoration: string, text: string) => + `${discord.decoration ? decoration : ""} ${text}`.trim(); + + const limitCharacter = 800; + const truncatedErrorMessage = errorMessage.substring(0, limitCharacter); + await sendDiscordNotification(discord, { + title: decorate(">", "`⚠️` Build Failed"), + color: 0xed4245, + 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: ``, + inline: true, + }, + { + name: decorate("`⌚`", "Time"), + value: ``, + 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) { + const decorate = (decoration: string, text: string) => + `${gotify.decoration ? decoration : ""} ${text}\n`; + await sendGotifyNotification( + gotify, + decorate("⚠️", "Build Failed"), + `${decorate("πŸ› οΈ", `Project: ${projectName}`)}` + + `${decorate("βš™οΈ", `Application: ${applicationName}`)}` + + `${decorate("❔", `Type: ${applicationType}`)}` + + `${decorate("πŸ•’", `Date: ${date.toLocaleString()}`)}` + + `${decorate("⚠️", `Error:\n${errorMessage}`)}` + + `${decorate("πŸ”—", `Build details:\n${buildLink}`)}`, + ); + } + + if (ntfy) { + await sendNtfyNotification( + ntfy, + "Build Failed", + "warning", + `view, Build details, ${buildLink}, clear=true;`, + `πŸ› οΈProject: ${projectName}\n` + + `βš™οΈApplication: ${applicationName}\n` + + `❔Type: ${applicationType}\n` + + `πŸ•’Date: ${date.toLocaleString()}\n` + + `⚠️Error:\n${errorMessage}`, + ); + } + + if (telegram) { + const inlineButton = [ + [ + { + text: "Deployment Logs", + url: buildLink, + }, + ], + ]; + + await sendTelegramNotification( + telegram, + `⚠️ Build Failed\n\nProject: ${projectName}\nApplication: ${applicationName}\nType: ${applicationType}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}\n\nError:\n
${errorMessage}
`, + inlineButton, + ); + } + + if (slack) { + const { channel } = slack; + await sendSlackNotification(slack, { + channel: channel, + attachments: [ + { + color: "#FF0000", + pretext: ":warning: *Build Failed*", + fields: [ + { + title: "Project", + value: projectName, + short: true, + }, + { + title: "Application", + value: applicationName, + short: true, + }, + { + title: "Type", + value: applicationType, + short: true, + }, + { + title: "Time", + value: date.toLocaleString(), + short: true, + }, + { + title: "Error", + value: `\`\`\`${errorMessage}\`\`\``, + short: false, + }, + ], + actions: [ + { + type: "button", + text: "View Build Details", + url: buildLink, + }, + ], + }, + ], + }); + } + + if (custom) { + await sendCustomNotification(custom, { + title: "Build Error", + message: "Build failed with errors", projectName, applicationName, applicationType, - errorMessage: errorMessage, + errorMessage, buildLink, + timestamp: date.toISOString(), date: date.toLocaleString(), - }), - ).catch(); - await sendEmailNotification(email, "Build failed for dokploy", template); - } + status: "error", + type: "build", + }); + } - if (discord) { - const decorate = (decoration: string, text: string) => - `${discord.decoration ? decoration : ""} ${text}`.trim(); - - const limitCharacter = 800; - const truncatedErrorMessage = errorMessage.substring(0, limitCharacter); - await sendDiscordNotification(discord, { - title: decorate(">", "`⚠️` Build Failed"), - color: 0xed4245, - 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: ``, - inline: true, - }, - { - name: decorate("`⌚`", "Time"), - value: ``, - 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) { - const decorate = (decoration: string, text: string) => - `${gotify.decoration ? decoration : ""} ${text}\n`; - await sendGotifyNotification( - gotify, - decorate("⚠️", "Build Failed"), - `${decorate("πŸ› οΈ", `Project: ${projectName}`)}` + - `${decorate("βš™οΈ", `Application: ${applicationName}`)}` + - `${decorate("❔", `Type: ${applicationType}`)}` + - `${decorate("πŸ•’", `Date: ${date.toLocaleString()}`)}` + - `${decorate("⚠️", `Error:\n${errorMessage}`)}` + - `${decorate("πŸ”—", `Build details:\n${buildLink}`)}`, - ); - } - - if (ntfy) { - await sendNtfyNotification( - ntfy, - "Build Failed", - "warning", - `view, Build details, ${buildLink}, clear=true;`, - `πŸ› οΈProject: ${projectName}\n` + - `βš™οΈApplication: ${applicationName}\n` + - `❔Type: ${applicationType}\n` + - `πŸ•’Date: ${date.toLocaleString()}\n` + - `⚠️Error:\n${errorMessage}`, - ); - } - - if (telegram) { - const inlineButton = [ - [ - { - text: "Deployment Logs", - url: buildLink, - }, - ], - ]; - - await sendTelegramNotification( - telegram, - `⚠️ Build Failed\n\nProject: ${projectName}\nApplication: ${applicationName}\nType: ${applicationType}\nDate: ${format( - date, - "PP", - )}\nTime: ${format( - date, - "pp", - )}\n\nError:\n
${errorMessage}
`, - inlineButton, - ); - } - - if (slack) { - const { channel } = slack; - await sendSlackNotification(slack, { - channel: channel, - attachments: [ - { - color: "#FF0000", - pretext: ":warning: *Build Failed*", - fields: [ - { - title: "Project", - value: projectName, - short: true, - }, - { - title: "Application", - value: applicationName, - short: true, - }, - { - title: "Type", - value: applicationType, - short: true, - }, - { - title: "Time", - value: date.toLocaleString(), - short: true, - }, - { - title: "Error", - value: `\`\`\`${errorMessage}\`\`\``, - short: false, - }, - ], - actions: [ - { - type: "button", - text: "View Build Details", - url: buildLink, - }, - ], - }, - ], - }); - } - - if (custom) { - await sendCustomNotification(custom, { - title: "Build Error", - message: "Build failed with errors", - projectName, - applicationName, - applicationType, - errorMessage, - buildLink, - timestamp: date.toISOString(), - date: date.toLocaleString(), - status: "error", - type: "build", - }); - } - - if (lark) { - const limitCharacter = 800; - const truncatedErrorMessage = errorMessage.substring(0, limitCharacter); - 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", + if (lark) { + const limitCharacter = 800; + const truncatedErrorMessage = errorMessage.substring(0, limitCharacter); + 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: { - title: { - tag: "plain_text", - 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, - }, - ], + header: { + title: { + tag: "plain_text", + content: "⚠️ Build Failed", }, - { - tag: "button", - text: { - tag: "plain_text", - content: "View Build Details", + 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, + }, + ], }, - type: "danger", - width: "default", - size: "medium", - behaviors: [ - { - type: "open_url", - default_url: buildLink, - pc_url: "", - ios_url: "", - android_url: "", + { + tag: "button", + text: { + tag: "plain_text", + content: "View Build Details", }, - ], - margin: "0px 0px 0px 0px", - }, - ], + type: "danger", + 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); } } }; diff --git a/packages/server/src/utils/notifications/build-success.ts b/packages/server/src/utils/notifications/build-success.ts index ab84a5fde..f3350d72b 100644 --- a/packages/server/src/utils/notifications/build-success.ts +++ b/packages/server/src/utils/notifications/build-success.ts @@ -6,337 +6,365 @@ import { renderAsync } from "@react-email/components"; import { format } from "date-fns"; import { and, eq } from "drizzle-orm"; import { - sendCustomNotification, - sendDiscordNotification, - sendEmailNotification, - sendGotifyNotification, - sendLarkNotification, - sendNtfyNotification, - sendSlackNotification, - sendTelegramNotification, + sendCustomNotification, + sendDiscordNotification, + sendEmailNotification, + sendGotifyNotification, + sendLarkNotification, + sendNtfyNotification, + sendSlackNotification, + sendTelegramNotification, } from "./utils"; interface Props { - projectName: string; - applicationName: string; - applicationType: string; - buildLink: string; - organizationId: string; - domains: Domain[]; + projectName: string; + applicationName: string; + applicationType: string; + buildLink: string; + organizationId: string; + domains: Domain[]; + environmentName: string; } export const sendBuildSuccessNotifications = async ({ - projectName, - applicationName, - applicationType, - buildLink, - organizationId, - domains, + projectName, + applicationName, + applicationType, + buildLink, + organizationId, + domains, + environmentName, }: Props) => { - const date = new Date(); - const unixDate = ~~(Number(date) / 1000); - const notificationList = await db.query.notifications.findMany({ - where: and( - eq(notifications.appDeploy, true), - eq(notifications.organizationId, organizationId), - ), - with: { - email: true, - discord: true, - telegram: true, - slack: true, - gotify: true, - ntfy: true, - custom: true, - lark: true, - }, - }); + const date = new Date(); + const unixDate = ~~(Number(date) / 1000); + const notificationList = await db.query.notifications.findMany({ + where: and( + eq(notifications.appDeploy, true), + eq(notifications.organizationId, organizationId) + ), + with: { + email: true, + discord: true, + telegram: true, + slack: true, + gotify: true, + ntfy: true, + custom: true, + lark: true, + }, + }); - for (const notification of notificationList) { - const { email, discord, telegram, slack, gotify, ntfy, custom, lark } = - notification; + for (const notification of notificationList) { + const { email, discord, telegram, slack, gotify, ntfy, custom, lark } = + notification; + try { + if (email) { + const template = await renderAsync( + BuildSuccessEmail({ + projectName, + applicationName, + applicationType, + buildLink, + date: date.toLocaleString(), + environmentName, + }) + ).catch(); + await sendEmailNotification( + email, + "Build success for dokploy", + template + ); + } - if (email) { - const template = await renderAsync( - BuildSuccessEmail({ - projectName, - applicationName, - applicationType, - buildLink, - date: date.toLocaleString(), - }), - ).catch(); - await sendEmailNotification(email, "Build success for dokploy", template); - } + if (discord) { + const decorate = (decoration: string, text: string) => + `${discord.decoration ? decoration : ""} ${text}`.trim(); - if (discord) { - const decorate = (decoration: string, text: string) => - `${discord.decoration ? decoration : ""} ${text}`.trim(); + 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: ``, + inline: true, + }, + { + name: decorate("`⌚`", "Time"), + value: ``, + 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", + }, + }); + } - 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: ``, - inline: true, - }, - { - name: decorate("`⌚`", "Time"), - value: ``, - 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("🌍", `Environment: ${environmentName}`)}` + + `${decorate("❔", `Type: ${applicationType}`)}` + + `${decorate("πŸ•’", `Date: ${date.toLocaleString()}`)}` + + `${decorate("πŸ”—", `Build details:\n${buildLink}`)}` + ); + } - 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` + + `🌍Environment: ${environmentName}\n` + + `❔Type: ${applicationType}\n` + + `πŸ•’Date: ${date.toLocaleString()}` + ); + } - 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 = (array: T[], chunkSize: number): T[][] => + Array.from({ length: Math.ceil(array.length / chunkSize) }, (_, i) => + array.slice(i * chunkSize, i * chunkSize + chunkSize) + ); - if (telegram) { - const chunkArray = (array: T[], chunkSize: number): T[][] => - Array.from({ length: Math.ceil(array.length / chunkSize) }, (_, i) => - array.slice(i * chunkSize, i * chunkSize + chunkSize), - ); + const inlineButton = [ + [ + { + text: "Deployment Logs", + url: buildLink, + }, + ], + ...chunkArray(domains, 2).map((chunk) => + chunk.map((data) => ({ + text: data.host, + url: `${data.https ? "https" : "http"}://${data.host}`, + })) + ), + ]; - const inlineButton = [ - [ - { - text: "Deployment Logs", - url: buildLink, - }, - ], - ...chunkArray(domains, 2).map((chunk) => - chunk.map((data) => ({ - text: data.host, - url: `${data.https ? "https" : "http"}://${data.host}`, - })), - ), - ]; + await sendTelegramNotification( + telegram, + `βœ… Build Success\n\nProject: ${projectName}\nApplication: ${applicationName}\nEnvironment: ${environmentName}\nType: ${applicationType}\nDate: ${format( + date, + "PP" + )}\nTime: ${format(date, "pp")}`, + inlineButton + ); + } - await sendTelegramNotification( - telegram, - `βœ… Build Success\n\nProject: ${projectName}\nApplication: ${applicationName}\nType: ${applicationType}\nDate: ${format( - date, - "PP", - )}\nTime: ${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 (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: "Type", - value: applicationType, - short: true, - }, - { - title: "Time", - value: date.toLocaleString(), - short: true, - }, - ], - actions: [ - { - type: "button", - text: "View Build Details", - url: buildLink, - }, - ], - }, - ], - }); - } + if (custom) { + await sendCustomNotification(custom, { + title: "Build Success", + message: "Build completed successfully", + projectName, + applicationName, + applicationType, + buildLink, + timestamp: date.toISOString(), + date: date.toLocaleString(), + domains: domains.map((domain) => domain.host).join(", "), + status: "success", + type: "build", + }); + } - if (custom) { - await sendCustomNotification(custom, { - title: "Build Success", - message: "Build completed successfully", - projectName, - applicationName, - applicationType, - buildLink, - timestamp: date.toISOString(), - date: date.toLocaleString(), - domains: domains.map((domain) => domain.host).join(", "), - status: "success", - type: "build", - }); - } - - 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: { - title: { - tag: "plain_text", - 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, - }, - ], - }, - { - tag: "button", - text: { - tag: "plain_text", - content: "View Build Details", - }, - type: "primary", - width: "default", - size: "medium", - behaviors: [ - { - type: "open_url", - default_url: buildLink, - pc_url: "", - ios_url: "", - android_url: "", - }, - ], - margin: "0px 0px 0px 0px", - }, - ], - }, - }, - }); - } - } + 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: { + title: { + tag: "plain_text", + 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: `**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, + }, + ], + }, + { + tag: "button", + text: { + tag: "plain_text", + content: "View Build Details", + }, + type: "primary", + 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); + } + } }; diff --git a/packages/server/src/utils/notifications/database-backup.ts b/packages/server/src/utils/notifications/database-backup.ts index fa4e3b253..e0754b715 100644 --- a/packages/server/src/utils/notifications/database-backup.ts +++ b/packages/server/src/utils/notifications/database-backup.ts @@ -54,328 +54,331 @@ export const sendDatabaseBackupNotifications = async ({ for (const notification of notificationList) { const { email, discord, telegram, slack, gotify, ntfy, custom, lark } = 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) { - const template = await renderAsync( - DatabaseBackupEmail({ + if (discord) { + const decorate = (decoration: string, text: string) => + `${discord.decoration ? decoration : ""} ${text}`.trim(); + + await sendDiscordNotification(discord, { + title: + 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: ``, + inline: true, + }, + { + name: decorate("`⌚`", "Time"), + value: ``, + 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) { + const decorate = (decoration: string, text: string) => + `${gotify.decoration ? decoration : ""} ${text}\n`; + + await sendGotifyNotification( + gotify, + 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}`) : ""}`, + ); + } + + if (ntfy) { + await sendNtfyNotification( + 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 isError = type === "error" && errorMessage; + + const statusEmoji = type === "success" ? "βœ…" : "❌"; + const typeStatus = type === "success" ? "Successful" : "Failed"; + const errorMsg = isError + ? `\n\nError:\n
${errorMessage}
` + : ""; + + const messageText = `${statusEmoji} Database Backup ${typeStatus}\n\nProject: ${projectName}\nApplication: ${applicationName}\nType: ${databaseType}\nDatabase Name: ${databaseName}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}${isError ? errorMsg : ""}`; + + 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 (custom) { + await sendCustomNotification(custom, { + title: `Database Backup ${type === "success" ? "Successful" : "Failed"}`, + message: + type === "success" + ? "Database backup completed successfully" + : "Database backup failed", projectName, applicationName, databaseType, + databaseName, type, - errorMessage, + errorMessage: errorMessage || "", + timestamp: date.toISOString(), date: date.toLocaleString(), - }), - ).catch(); - await sendEmailNotification( - email, - "Database backup for dokploy", - template, - ); - } + status: type, + }); + } - if (discord) { - const decorate = (decoration: string, text: string) => - `${discord.decoration ? decoration : ""} ${text}`.trim(); + if (lark) { + const limitCharacter = 800; + const truncatedErrorMessage = + errorMessage && errorMessage.length > limitCharacter + ? errorMessage.substring(0, limitCharacter) + : errorMessage; - await sendDiscordNotification(discord, { - title: - 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: ``, - inline: true, - }, - { - name: decorate("`⌚`", "Time"), - value: ``, - inline: true, - }, - { - name: decorate("`❓`", "Type"), - value: type - .replace("error", "Failed") - .replace("success", "Successful"), - inline: true, - }, - ...(type === "error" && errorMessage - ? [ + 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: { + title: { + tag: "plain_text", + content: + type === "success" + ? "βœ… Database Backup Successful" + : "❌ 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: [ { - name: decorate("`⚠️`", "Error Message"), - value: `\`\`\`${errorMessage}\`\`\``, - }, - ] - : []), - ], - timestamp: date.toISOString(), - footer: { - text: "Dokploy Database Backup Notification", - }, - }); - } - - if (gotify) { - const decorate = (decoration: string, text: string) => - `${gotify.decoration ? decoration : ""} ${text}\n`; - - await sendGotifyNotification( - gotify, - 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}`) : ""}`, - ); - } - - if (ntfy) { - await sendNtfyNotification( - 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 isError = type === "error" && errorMessage; - - const statusEmoji = type === "success" ? "βœ…" : "❌"; - const typeStatus = type === "success" ? "Successful" : "Failed"; - const errorMsg = isError - ? `\n\nError:\n
${errorMessage}
` - : ""; - - const messageText = `${statusEmoji} Database Backup ${typeStatus}\n\nProject: ${projectName}\nApplication: ${applicationName}\nType: ${databaseType}\nDatabase Name: ${databaseName}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}${isError ? errorMsg : ""}`; - - 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 - ? [ + tag: "column_set", + columns: [ { - title: "Error Message", - value: errorMessage, - short: false, + 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, }, - ] - : []), - { - 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 (custom) { - await sendCustomNotification(custom, { - title: `Database Backup ${type === "success" ? "Successful" : "Failed"}`, - message: - type === "success" - ? "Database backup completed successfully" - : "Database backup failed", - projectName, - applicationName, - databaseType, - databaseName, - type, - errorMessage: errorMessage || "", - timestamp: date.toISOString(), - date: date.toLocaleString(), - status: type, - }); - } - - if (lark) { - const limitCharacter = 800; - const truncatedErrorMessage = - errorMessage && errorMessage.length > limitCharacter - ? errorMessage.substring(0, limitCharacter) - : errorMessage; - - 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: { - title: { - tag: "plain_text", - content: - type === "success" - ? "βœ… Database Backup Successful" - : "❌ 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 - ? [ { - tag: "markdown", - content: `**Error Message:**\n\`\`\`\n${truncatedErrorMessage}\n\`\`\``, - text_align: "left", - text_size: "normal_v2", + 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); } } }; diff --git a/packages/server/src/utils/notifications/docker-cleanup.ts b/packages/server/src/utils/notifications/docker-cleanup.ts index cd25bdd4c..061f892ff 100644 --- a/packages/server/src/utils/notifications/docker-cleanup.ts +++ b/packages/server/src/utils/notifications/docker-cleanup.ts @@ -41,194 +41,197 @@ export const sendDockerCleanupNotifications = async ( for (const notification of notificationList) { const { email, discord, telegram, slack, gotify, ntfy, custom, lark } = notification; + try { + if (email) { + const template = await renderAsync( + DockerCleanupEmail({ message, date: date.toLocaleString() }), + ).catch(); - if (email) { - const template = await renderAsync( - DockerCleanupEmail({ message, date: date.toLocaleString() }), - ).catch(); + await sendEmailNotification( + email, + "Docker cleanup for dokploy", + template, + ); + } - await sendEmailNotification( - email, - "Docker cleanup for dokploy", - template, - ); - } + if (discord) { + const decorate = (decoration: string, text: string) => + `${discord.decoration ? decoration : ""} ${text}`.trim(); - if (discord) { - const decorate = (decoration: string, text: string) => - `${discord.decoration ? decoration : ""} ${text}`.trim(); - - await sendDiscordNotification(discord, { - title: decorate(">", "`βœ…` Docker Cleanup"), - color: 0x57f287, - fields: [ - { - name: decorate("`πŸ“…`", "Date"), - value: ``, - inline: true, + await sendDiscordNotification(discord, { + title: decorate(">", "`βœ…` Docker Cleanup"), + color: 0x57f287, + fields: [ + { + name: decorate("`πŸ“…`", "Date"), + value: ``, + inline: true, + }, + { + name: decorate("`⌚`", "Time"), + value: ``, + 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: ``, - 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) { - const decorate = (decoration: string, text: string) => - `${gotify.decoration ? decoration : ""} ${text}\n`; - await sendGotifyNotification( - gotify, - decorate("βœ…", "Docker Cleanup"), - `${decorate("πŸ•’", `Date: ${date.toLocaleString()}`)}` + - `${decorate("πŸ“œ", `Message:\n${message}`)}`, - ); - } + if (gotify) { + const decorate = (decoration: string, text: string) => + `${gotify.decoration ? decoration : ""} ${text}\n`; + await sendGotifyNotification( + gotify, + decorate("βœ…", "Docker Cleanup"), + `${decorate("πŸ•’", `Date: ${date.toLocaleString()}`)}` + + `${decorate("πŸ“œ", `Message:\n${message}`)}`, + ); + } - if (ntfy) { - await sendNtfyNotification( - ntfy, - "Docker Cleanup", - "white_check_mark", - "", - `πŸ•’Date: ${date.toLocaleString()}\n` + `πŸ“œMessage:\n${message}`, - ); - } + if (ntfy) { + await sendNtfyNotification( + ntfy, + "Docker Cleanup", + "white_check_mark", + "", + `πŸ•’Date: ${date.toLocaleString()}\n` + `πŸ“œMessage:\n${message}`, + ); + } - if (telegram) { - await sendTelegramNotification( - telegram, - `βœ… Docker Cleanup\n\nMessage: ${message}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}`, - ); - } + if (telegram) { + await sendTelegramNotification( + telegram, + `βœ… Docker Cleanup\n\nMessage: ${message}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}`, + ); + } - if (slack) { - const { channel } = slack; - await sendSlackNotification(slack, { - channel: channel, - attachments: [ - { - color: "#00FF00", - pretext: ":white_check_mark: *Docker Cleanup*", - fields: [ - { - title: "Message", - value: message, - }, - { - title: "Time", - value: date.toLocaleString(), - short: true, - }, - ], - }, - ], - }); - } + if (slack) { + const { channel } = slack; + await sendSlackNotification(slack, { + channel: channel, + attachments: [ + { + color: "#00FF00", + pretext: ":white_check_mark: *Docker Cleanup*", + fields: [ + { + title: "Message", + value: message, + }, + { + title: "Time", + value: date.toLocaleString(), + short: true, + }, + ], + }, + ], + }); + } - if (custom) { - await sendCustomNotification(custom, { - title: "Docker Cleanup", - message: "Docker cleanup completed successfully", - cleanupMessage: message, - timestamp: date.toISOString(), - date: date.toLocaleString(), - status: "success", - type: "docker-cleanup", - }); - } + if (custom) { + await sendCustomNotification(custom, { + title: "Docker Cleanup", + message: "Docker cleanup completed successfully", + cleanupMessage: message, + timestamp: date.toISOString(), + date: date.toLocaleString(), + status: "success", + type: "docker-cleanup", + }); + } - 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", + 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: { - title: { - tag: "plain_text", - 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, - }, - ], + header: { + title: { + tag: "plain_text", + 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, + }, + ], + }, + ], + }, }, - }, - }); + }); + } + } catch (error) { + console.log(error); } } }; diff --git a/packages/server/src/utils/notifications/dokploy-restart.ts b/packages/server/src/utils/notifications/dokploy-restart.ts index 97ea3cbc3..edc83c513 100644 --- a/packages/server/src/utils/notifications/dokploy-restart.ts +++ b/packages/server/src/utils/notifications/dokploy-restart.ts @@ -5,231 +5,222 @@ import { renderAsync } from "@react-email/components"; import { format } from "date-fns"; import { eq } from "drizzle-orm"; import { - sendCustomNotification, - sendDiscordNotification, - sendEmailNotification, - sendGotifyNotification, - sendLarkNotification, - sendNtfyNotification, - sendSlackNotification, - sendTelegramNotification, + sendCustomNotification, + sendDiscordNotification, + sendEmailNotification, + sendGotifyNotification, + sendLarkNotification, + sendNtfyNotification, + sendSlackNotification, + sendTelegramNotification, } from "./utils"; export const sendDokployRestartNotifications = async () => { - const date = new Date(); - const unixDate = ~~(Number(date) / 1000); - const notificationList = await db.query.notifications.findMany({ - where: eq(notifications.dokployRestart, true), - with: { - email: true, - discord: true, - telegram: true, - slack: true, - gotify: true, - ntfy: true, - custom: true, - lark: true, - }, - }); + const date = new Date(); + const unixDate = ~~(Number(date) / 1000); + const notificationList = await db.query.notifications.findMany({ + where: eq(notifications.dokployRestart, true), + with: { + email: true, + discord: true, + telegram: true, + slack: true, + gotify: true, + ntfy: true, + custom: true, + lark: true, + }, + }); - for (const notification of notificationList) { - const { email, discord, telegram, slack, gotify, ntfy, custom, lark } = - notification; + for (const notification of notificationList) { + const { email, discord, telegram, slack, gotify, ntfy, custom, lark } = + notification; - if (email) { - const template = await renderAsync( - DokployRestartEmail({ date: date.toLocaleString() }), - ).catch(); - await sendEmailNotification(email, "Dokploy Server Restarted", template); - } + try { + if (email) { + const template = await renderAsync( + DokployRestartEmail({ date: date.toLocaleString() }) + ).catch(); - if (discord) { - const decorate = (decoration: string, text: string) => - `${discord.decoration ? decoration : ""} ${text}`.trim(); + await sendEmailNotification( + email, + "Dokploy Server Restarted", + template + ); + } - try { - await sendDiscordNotification(discord, { - title: decorate(">", "`βœ…` Dokploy Server Restarted"), - color: 0x57f287, - fields: [ - { - name: decorate("`πŸ“…`", "Date"), - value: ``, - inline: true, - }, - { - name: decorate("`⌚`", "Time"), - value: ``, - inline: true, - }, - { - name: decorate("`❓`", "Type"), - value: "Successful", - inline: true, - }, - ], - timestamp: date.toISOString(), - footer: { - text: "Dokploy Restart Notification", - }, - }); - } catch (error) { - console.log(error); - } - } + if (discord) { + const decorate = (decoration: string, text: string) => + `${discord.decoration ? decoration : ""} ${text}`.trim(); - if (gotify) { - const decorate = (decoration: string, text: string) => - `${gotify.decoration ? decoration : ""} ${text}\n`; - try { - await sendGotifyNotification( - gotify, - decorate("βœ…", "Dokploy Server Restarted"), - `${decorate("πŸ•’", `Date: ${date.toLocaleString()}`)}`, - ); - } catch (error) { - console.log(error); - } - } + await sendDiscordNotification(discord, { + title: decorate(">", "`βœ…` Dokploy Server Restarted"), + color: 0x57f287, + fields: [ + { + name: decorate("`πŸ“…`", "Date"), + value: ``, + inline: true, + }, + { + name: decorate("`⌚`", "Time"), + value: ``, + inline: true, + }, + { + name: decorate("`❓`", "Type"), + value: "Successful", + inline: true, + }, + ], + timestamp: date.toISOString(), + footer: { + text: "Dokploy Restart Notification", + }, + }); + } - if (ntfy) { - try { - await sendNtfyNotification( - ntfy, - "Dokploy Server Restarted", - "white_check_mark", - "", - `πŸ•’Date: ${date.toLocaleString()}`, - ); - } catch (error) { - console.log(error); - } - } + if (gotify) { + const decorate = (decoration: string, text: string) => + `${gotify.decoration ? decoration : ""} ${text}\n`; + await sendGotifyNotification( + gotify, + decorate("βœ…", "Dokploy Server Restarted"), + `${decorate("πŸ•’", `Date: ${date.toLocaleString()}`)}` + ); + } - if (telegram) { - try { - await sendTelegramNotification( - telegram, - `βœ… Dokploy Server Restarted\n\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}`, - ); - } catch (error) { - console.log(error); - } - } + if (ntfy) { + await sendNtfyNotification( + ntfy, + "Dokploy Server Restarted", + "white_check_mark", + "", + `πŸ•’Date: ${date.toLocaleString()}` + ); + } - if (slack) { - const { channel } = slack; - try { - await sendSlackNotification(slack, { - channel: channel, - attachments: [ - { - color: "#00FF00", - pretext: ":white_check_mark: *Dokploy Server Restarted*", - fields: [ - { - title: "Time", - value: date.toLocaleString(), - short: true, - }, - ], - }, - ], - }); - } catch (error) { - console.log(error); - } - } + if (telegram) { + await sendTelegramNotification( + telegram, + `βœ… Dokploy Server Restarted\n\nDate: ${format( + date, + "PP" + )}\nTime: ${format(date, "pp")}` + ); + } - if (custom) { - try { - await sendCustomNotification(custom, { - title: "Dokploy Server Restarted", - message: "Dokploy server has been restarted successfully", - timestamp: date.toISOString(), - date: date.toLocaleString(), - status: "success", - type: "dokploy-restart", - }); - } catch (error) { - console.log(error); - } - } + if (slack) { + const { channel } = slack; + await sendSlackNotification(slack, { + channel: channel, + attachments: [ + { + color: "#00FF00", + pretext: ":white_check_mark: *Dokploy Server Restarted*", + fields: [ + { + title: "Time", + value: date.toLocaleString(), + short: true, + }, + ], + }, + ], + }); + } - if (lark) { - try { - 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: { - title: { - tag: "plain_text", - content: "βœ… Dokploy Server Restarted", - }, - 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", - }, - ], - vertical_align: "top", - weight: 1, - }, - { - tag: "column", - width: "weighted", - elements: [ - { - tag: "markdown", - content: `**Restart Time:**\n${format(date, "PP pp")}`, - text_align: "left", - text_size: "normal_v2", - }, - ], - vertical_align: "top", - weight: 1, - }, - ], - }, - ], - }, - }, - }); - } catch (error) { - console.log(error); - } - } - } + if (custom) { + try { + await sendCustomNotification(custom, { + title: "Dokploy Server Restarted", + message: "Dokploy server has been restarted successfully", + timestamp: date.toISOString(), + date: date.toLocaleString(), + status: "success", + type: "dokploy-restart", + }); + } catch (error) { + console.log(error); + } + } + + 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: { + title: { + tag: "plain_text", + content: "βœ… Dokploy Server Restarted", + }, + 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", + }, + ], + vertical_align: "top", + weight: 1, + }, + { + tag: "column", + width: "weighted", + elements: [ + { + tag: "markdown", + content: `**Restart Time:**\n${format( + date, + "PP pp" + )}`, + text_align: "left", + text_size: "normal_v2", + }, + ], + vertical_align: "top", + weight: 1, + }, + ], + }, + ], + }, + }, + }); + } + } catch (error) { + console.log(error); + } + } }; diff --git a/packages/server/src/utils/notifications/utils.ts b/packages/server/src/utils/notifications/utils.ts index 23d976489..f20196ccc 100644 --- a/packages/server/src/utils/notifications/utils.ts +++ b/packages/server/src/utils/notifications/utils.ts @@ -39,6 +39,9 @@ export const sendEmailNotification = async ( }); } catch (err) { console.log(err); + throw new Error( + `Failed to send email notification ${err instanceof Error ? err.message : "Unknown error"}`, + ); } }; @@ -46,15 +49,23 @@ export const sendDiscordNotification = async ( connection: typeof discord.$inferInsert, embed: any, ) => { - // try { - await fetch(connection.webhookUrl, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ embeds: [embed] }), - }); - // } catch (err) { - // console.log(err); - // } + try { + const response = await fetch(connection.webhookUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ embeds: [embed] }), + }); + if (!response.ok) { + 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 ( @@ -91,13 +102,21 @@ export const sendSlackNotification = async ( message: any, ) => { try { - await fetch(connection.webhookUrl, { + const response = await fetch(connection.webhookUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(message), }); + if (!response.ok) { + throw new Error( + `Failed to send slack notification ${response.statusText}`, + ); + } } catch (err) { - console.log(err); + console.log("error", err); + throw new Error( + `Failed to send slack notification ${err instanceof Error ? err.message : "Unknown error"}`, + ); } }; diff --git a/packages/server/src/utils/process/ExecError.ts b/packages/server/src/utils/process/ExecError.ts new file mode 100644 index 000000000..773968b5c --- /dev/null +++ b/packages/server/src/utils/process/ExecError.ts @@ -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; + } +} diff --git a/packages/server/src/utils/process/execAsync.ts b/packages/server/src/utils/process/execAsync.ts index 84f0701d9..cd0249000 100644 --- a/packages/server/src/utils/process/execAsync.ts +++ b/packages/server/src/utils/process/execAsync.ts @@ -2,8 +2,43 @@ import { exec, execFile } from "node:child_process"; import util from "node:util"; import { findServerById } from "@dokploy/server/services/server"; 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 { cwd?: string; @@ -21,7 +56,16 @@ export const execAsyncStream = ( const childProcess = exec(command, options, (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; } resolve({ stdout: stdoutComplete, stderr: stderrComplete }); @@ -45,7 +89,14 @@ export const execAsyncStream = ( childProcess.on("error", (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) => { if (err) { onData?.(err.message); - throw err; + reject( + new ExecError(`Remote command execution failed: ${err.message}`, { + command, + serverId, + originalError: err, + }), + ); + return; } stream .on("close", (code: number, _signal: string) => { @@ -117,8 +175,15 @@ export const execAsyncRemote = async ( resolve({ stdout, stderr }); } else { reject( - new Error( - `Command exited with code ${code}. Stderr: ${stderr}, command: ${command}`, + new ExecError( + `Remote command failed with exit code ${code}`, + { + command, + stdout, + stderr, + exitCode: code, + serverId, + }, ), ); } @@ -136,17 +201,25 @@ export const execAsyncRemote = async ( .on("error", (err) => { conn.end(); if (err.level === "client-authentication") { - onData?.( - `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`, - ); + const errorMsg = `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`; + onData?.(errorMsg); reject( - new Error( - `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`, - ), + new ExecError(errorMsg, { + command, + serverId, + originalError: err, + }), ); } else { - onData?.(`SSH connection error: ${err.message}`); - reject(new Error(`SSH connection error: ${err.message}`)); + const errorMsg = `SSH connection error: ${err.message}`; + onData?.(errorMsg); + reject( + new ExecError(errorMsg, { + command, + serverId, + originalError: err, + }), + ); } }) .connect({ diff --git a/packages/server/src/utils/providers/bitbucket.ts b/packages/server/src/utils/providers/bitbucket.ts index ed6cd8c31..2248baaaf 100644 --- a/packages/server/src/utils/providers/bitbucket.ts +++ b/packages/server/src/utils/providers/bitbucket.ts @@ -1,4 +1,3 @@ -import { createWriteStream } from "node:fs"; import { join } from "node:path"; import { paths } from "@dokploy/server/constants"; import type { @@ -9,12 +8,8 @@ import { type Bitbucket, findBitbucketById, } from "@dokploy/server/services/bitbucket"; -import type { Compose } from "@dokploy/server/services/compose"; import type { InferResultType } from "@dokploy/server/types/with"; import { TRPCError } from "@trpc/server"; -import { recreateDirectory } from "../filesystem/directory"; -import { execAsyncRemote } from "../process/execAsync"; -import { spawnAsync } from "../process/spawnAsync"; export type ApplicationWithBitbucket = InferResultType< "applications", @@ -81,202 +76,52 @@ export const getBitbucketHeaders = (bitbucketProvider: Bitbucket) => { }; }; -export const cloneBitbucketRepository = async ( - entity: ApplicationWithBitbucket | ComposeWithBitbucket, - logPath: string, - isCompose = false, -) => { - const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(); - const writeStream = createWriteStream(logPath, { flags: "a" }); +interface CloneBitbucketRepository { + appName: string; + bitbucketRepository: string | null; + bitbucketOwner: string | null; + bitbucketBranch: string | null; + bitbucketId: string | null; + enableSubmodules: boolean; + serverId: string | null; + type?: "application" | "compose"; +} + +export const cloneBitbucketRepository = async ({ + type = "application", + ...entity +}: CloneBitbucketRepository) => { + let command = "set -e;"; const { appName, bitbucketRepository, bitbucketOwner, bitbucketBranch, bitbucketId, - bitbucket, enableSubmodules, + serverId, } = entity; + const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(!!serverId); if (!bitbucketId) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Bitbucket Provider not found", - }); + command += `echo "Error: ❌ Bitbucket Provider not found"; exit 1;`; + return command; } + const bitbucket = await findBitbucketById(bitbucketId); - const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH; + if (!bitbucket) { + command += `echo "Error: ❌ Bitbucket Provider not found"; exit 1;`; + return command; + } + const basePath = type === "compose" ? COMPOSE_PATH : APPLICATIONS_PATH; const outputPath = join(basePath, appName, "code"); - await recreateDirectory(outputPath); + command += `rm -rf ${outputPath};`; + command += `mkdir -p ${outputPath};`; const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`; const cloneUrl = getBitbucketCloneUrl(bitbucket, repoclone); - try { - writeStream.write(`\nCloning Repo ${repoclone} to ${outputPath}: βœ…\n`); - const cloneArgs = [ - "clone", - "--branch", - bitbucketBranch!, - "--depth", - "1", - ...(enableSubmodules ? ["--recurse-submodules"] : []), - cloneUrl, - outputPath, - "--progress", - ]; - - await spawnAsync("git", cloneArgs, (data) => { - if (writeStream.writable) { - writeStream.write(data); - } - }); - writeStream.write(`\nCloned ${repoclone} to ${outputPath}: βœ…\n`); - } catch (error) { - writeStream.write(`ERROR Cloning: ${error}: ❌`); - throw error; - } finally { - writeStream.end(); - } -}; - -export const cloneRawBitbucketRepository = async (entity: Compose) => { - const { COMPOSE_PATH } = paths(); - const { - appName, - bitbucketRepository, - bitbucketOwner, - bitbucketBranch, - bitbucketId, - enableSubmodules, - } = entity; - - if (!bitbucketId) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Bitbucket Provider not found", - }); - } - - const bitbucketProvider = await findBitbucketById(bitbucketId); - const basePath = COMPOSE_PATH; - const outputPath = join(basePath, appName, "code"); - await recreateDirectory(outputPath); - const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`; - const cloneUrl = getBitbucketCloneUrl(bitbucketProvider, repoclone); - - try { - const cloneArgs = [ - "clone", - "--branch", - bitbucketBranch!, - "--depth", - "1", - ...(enableSubmodules ? ["--recurse-submodules"] : []), - cloneUrl, - outputPath, - "--progress", - ]; - - await spawnAsync("git", cloneArgs); - } catch (error) { - throw error; - } -}; - -export const cloneRawBitbucketRepositoryRemote = async (compose: Compose) => { - const { COMPOSE_PATH } = paths(true); - const { - appName, - bitbucketRepository, - bitbucketOwner, - bitbucketBranch, - bitbucketId, - serverId, - enableSubmodules, - } = compose; - - if (!serverId) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Server not found", - }); - } - if (!bitbucketId) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Bitbucket Provider not found", - }); - } - - const bitbucketProvider = await findBitbucketById(bitbucketId); - const basePath = COMPOSE_PATH; - const outputPath = join(basePath, appName, "code"); - const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`; - const cloneUrl = getBitbucketCloneUrl(bitbucketProvider, repoclone); - - try { - const cloneCommand = ` - rm -rf ${outputPath}; - git clone --branch ${bitbucketBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} - `; - await execAsyncRemote(serverId, cloneCommand); - } catch (error) { - throw error; - } -}; - -export const getBitbucketCloneCommand = async ( - entity: ApplicationWithBitbucket | ComposeWithBitbucket, - logPath: string, - isCompose = false, -) => { - const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(true); - const { - appName, - bitbucketRepository, - bitbucketOwner, - bitbucketBranch, - bitbucketId, - serverId, - enableSubmodules, - } = entity; - - if (!serverId) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Server not found", - }); - } - - if (!bitbucketId) { - const command = ` - echo "Error: ❌ Bitbucket Provider not found" >> ${logPath}; - exit 1; - `; - await execAsyncRemote(serverId, command); - throw new TRPCError({ - code: "NOT_FOUND", - message: "Bitbucket Provider not found", - }); - } - - const bitbucketProvider = await findBitbucketById(bitbucketId); - const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH; - const outputPath = join(basePath, appName, "code"); - await recreateDirectory(outputPath); - const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`; - const cloneUrl = getBitbucketCloneUrl(bitbucketProvider, repoclone); - - const cloneCommand = ` -rm -rf ${outputPath}; -mkdir -p ${outputPath}; -if ! git clone --branch ${bitbucketBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then - echo "❌ [ERROR] Fail to clone the repository ${repoclone}" >> ${logPath}; - exit 1; -fi -echo "Cloned ${repoclone} to ${outputPath}: βœ…" >> ${logPath}; - `; - - return cloneCommand; + command += `echo "Cloning Repo ${repoclone} to ${outputPath}: βœ…";`; + command += `git clone --branch ${bitbucketBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} --progress;`; + return command; }; export const getBitbucketRepositories = async (bitbucketId?: string) => { diff --git a/packages/server/src/utils/providers/docker.ts b/packages/server/src/utils/providers/docker.ts index 56341b7d6..06f962dc7 100644 --- a/packages/server/src/utils/providers/docker.ts +++ b/packages/server/src/utils/providers/docker.ts @@ -1,60 +1,6 @@ -import { createWriteStream } from "node:fs"; -import { type ApplicationNested, mechanizeDockerContainer } from "../builders"; -import { pullImage } from "../docker/utils"; +import type { ApplicationNested } from "../builders"; -interface RegistryAuth { - username: string; - password: string; - registryUrl: string; -} - -export const buildDocker = async ( - application: ApplicationNested, - logPath: string, -): Promise => { - const { buildType, dockerImage, username, password } = application; - const authConfig: Partial = { - username: username || "", - password: password || "", - registryUrl: application.registryUrl || "", - }; - - const writeStream = createWriteStream(logPath, { flags: "a" }); - - writeStream.write(`\nBuild ${buildType}\n`); - - writeStream.write(`Pulling ${dockerImage}: βœ…\n`); - - try { - if (!dockerImage) { - throw new Error("Docker image not found"); - } - - await pullImage( - dockerImage, - (data) => { - if (writeStream.writable) { - writeStream.write(`${data}\n`); - } - }, - authConfig, - ); - await mechanizeDockerContainer(application); - writeStream.write("\nDocker Deployed: βœ…\n"); - } catch (error) { - writeStream.write( - `❌ Error: ${error instanceof Error ? error.message : String(error)}`, - ); - throw error; - } finally { - writeStream.end(); - } -}; - -export const buildRemoteDocker = async ( - application: ApplicationNested, - logPath: string, -) => { +export const buildRemoteDocker = async (application: ApplicationNested) => { const { registryUrl, dockerImage, username, password } = application; try { @@ -62,25 +8,25 @@ export const buildRemoteDocker = async ( throw new Error("Docker image not found"); } let command = ` -echo "Pulling ${dockerImage}" >> ${logPath}; +echo "Pulling ${dockerImage}"; `; if (username && password) { command += ` -if ! echo "${password}" | docker login --username "${username}" --password-stdin "${registryUrl || ""}" >> ${logPath} 2>&1; then - echo "❌ Login failed" >> ${logPath}; +if ! echo "${password}" | docker login --username "${username}" --password-stdin "${registryUrl || ""}" 2>&1; then + echo "❌ Login failed"; exit 1; fi `; } command += ` -docker pull ${dockerImage} >> ${logPath} 2>> ${logPath} || { - echo "❌ Pulling image failed" >> ${logPath}; +docker pull ${dockerImage} 2>&1 || { + echo "❌ Pulling image failed"; exit 1; } -echo "βœ… Pulling image completed." >> ${logPath}; +echo "βœ… Pulling image completed."; `; return command; } catch (error) { diff --git a/packages/server/src/utils/providers/git.ts b/packages/server/src/utils/providers/git.ts index 5779252db..8e640892d 100644 --- a/packages/server/src/utils/providers/git.ts +++ b/packages/server/src/utils/providers/git.ts @@ -1,159 +1,65 @@ -import { createWriteStream } from "node:fs"; import path, { join } from "node:path"; import { paths } from "@dokploy/server/constants"; -import type { Compose } from "@dokploy/server/services/compose"; import { findSSHKeyById, updateSSHKeyById, } from "@dokploy/server/services/ssh-key"; -import { TRPCError } from "@trpc/server"; -import { recreateDirectory } from "../filesystem/directory"; import { execAsync, execAsyncRemote } from "../process/execAsync"; -import { spawnAsync } from "../process/spawnAsync"; -export const cloneGitRepository = async ( - entity: { - appName: string; - customGitUrl?: string | null; - customGitBranch?: string | null; - customGitSSHKeyId?: string | null; - enableSubmodules?: boolean; - }, - logPath: string, - isCompose = false, -) => { - const { SSH_PATH, COMPOSE_PATH, APPLICATIONS_PATH } = paths(); +interface CloneGitRepository { + appName: string; + customGitUrl?: string | null; + customGitBranch?: string | null; + customGitSSHKeyId?: string | null; + enableSubmodules?: boolean; + serverId: string | null; + type?: "application" | "compose"; +} + +export const cloneGitRepository = async ({ + type = "application", + ...entity +}: CloneGitRepository) => { + let command = "set -e;"; const { appName, customGitUrl, customGitBranch, customGitSSHKeyId, enableSubmodules, + serverId, } = entity; + const { SSH_PATH, COMPOSE_PATH, APPLICATIONS_PATH } = paths(!!serverId); if (!customGitUrl || !customGitBranch) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error: Repository not found", - }); + command += `echo "Error: ❌ Repository not found"; exit 1;`; + return command; } - const writeStream = createWriteStream(logPath, { flags: "a" }); const temporalKeyPath = path.join("/tmp", "id_rsa"); if (customGitSSHKeyId) { const sshKey = await findSSHKeyById(customGitSSHKeyId); - await execAsync(` + command += ` echo "${sshKey.privateKey}" > ${temporalKeyPath} - chmod 600 ${temporalKeyPath} - `); + chmod 600 ${temporalKeyPath}; + `; } - const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH; + const basePath = type === "compose" ? COMPOSE_PATH : APPLICATIONS_PATH; const outputPath = join(basePath, appName, "code"); const knownHostsPath = path.join(SSH_PATH, "known_hosts"); - try { - if (!isHttpOrHttps(customGitUrl)) { - if (!customGitSSHKeyId) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: - "Error: you are trying to clone a ssh repository without a ssh key, please set a ssh key", - }); - } - await addHostToKnownHosts(customGitUrl); + if (!isHttpOrHttps(customGitUrl)) { + if (!customGitSSHKeyId) { + command += `echo "Error: ❌ You are trying to clone a ssh repository without a ssh key, please set a ssh key"; exit 1;`; + return command; } - await recreateDirectory(outputPath); - writeStream.write( - `\nCloning Repo Custom ${customGitUrl} to ${outputPath}: βœ…\n`, - ); - - if (customGitSSHKeyId) { - await updateSSHKeyById({ - sshKeyId: customGitSSHKeyId, - lastUsedAt: new Date().toISOString(), - }); - } - - const { port } = sanitizeRepoPathSSH(customGitUrl); - const cloneArgs = [ - "clone", - "--branch", - customGitBranch, - "--depth", - "1", - ...(enableSubmodules ? ["--recurse-submodules"] : []), - customGitUrl, - outputPath, - "--progress", - ]; - - await spawnAsync( - "git", - cloneArgs, - (data) => { - if (writeStream.writable) { - writeStream.write(data); - } - }, - { - env: { - ...process.env, - ...(customGitSSHKeyId && { - GIT_SSH_COMMAND: `ssh -i ${temporalKeyPath}${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`, - }), - }, - }, - ); - - writeStream.write(`\nCloned Custom Git ${customGitUrl}: βœ…\n`); - } catch (error) { - writeStream.write(`\nERROR Cloning Custom Git: ${error}: ❌\n`); - throw error; - } finally { - writeStream.end(); + command += addHostToKnownHostsCommand(customGitUrl); } -}; - -export const getCustomGitCloneCommand = async ( - entity: { - appName: string; - customGitUrl?: string | null; - customGitBranch?: string | null; - customGitSSHKeyId?: string | null; - serverId: string | null; - enableSubmodules: boolean; - }, - logPath: string, - isCompose = false, -) => { - const { SSH_PATH, COMPOSE_PATH, APPLICATIONS_PATH } = paths(true); - const { - appName, - customGitUrl, - customGitBranch, - customGitSSHKeyId, - serverId, - enableSubmodules, - } = entity; - - if (!customGitUrl || !customGitBranch) { - const command = ` - echo "Error: ❌ Repository not found" >> ${logPath}; - exit 1; - `; - - await execAsyncRemote(serverId, command); - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error: Repository not found", - }); - } - - const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH; - const outputPath = join(basePath, appName, "code"); - const knownHostsPath = path.join(SSH_PATH, "known_hosts"); + command += `rm -rf ${outputPath};`; + command += `mkdir -p ${outputPath};`; + command += `echo "Cloning Repo Custom ${customGitUrl} to ${outputPath}: βœ…";`; if (customGitSSHKeyId) { await updateSSHKeyById({ @@ -161,48 +67,22 @@ export const getCustomGitCloneCommand = async ( lastUsedAt: new Date().toISOString(), }); } - try { - const command = []; - if (!isHttpOrHttps(customGitUrl)) { - if (!customGitSSHKeyId) { - command.push( - `echo "Error: you are trying to clone a ssh repository without a ssh key, please set a ssh key ❌" >> ${logPath}; - exit 1; - `, - ); - } - command.push(addHostToKnownHostsCommand(customGitUrl)); - } - command.push(`rm -rf ${outputPath};`); - command.push(`mkdir -p ${outputPath};`); - command.push( - `echo "Cloning Custom Git ${customGitUrl}" to ${outputPath}: βœ… >> ${logPath};`, - ); - if (customGitSSHKeyId) { - const sshKey = await findSSHKeyById(customGitSSHKeyId); - const { port } = sanitizeRepoPathSSH(customGitUrl); - const gitSshCommand = `ssh -i /tmp/id_rsa${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`; - command.push( - ` - echo "${sshKey.privateKey}" > /tmp/id_rsa - chmod 600 /tmp/id_rsa - export GIT_SSH_COMMAND="${gitSshCommand}" - `, - ); - } - command.push( - `if ! git clone --branch ${customGitBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${customGitUrl} ${outputPath} >> ${logPath} 2>&1; then - echo "❌ [ERROR] Fail to clone the repository ${customGitUrl}" >> ${logPath}; + if (customGitSSHKeyId) { + const sshKey = await findSSHKeyById(customGitSSHKeyId); + const { port } = sanitizeRepoPathSSH(customGitUrl); + const gitSshCommand = `ssh -i /tmp/id_rsa${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`; + command += `echo "${sshKey.privateKey}" > /tmp/id_rsa;`; + command += "chmod 600 /tmp/id_rsa;"; + command += `export GIT_SSH_COMMAND="${gitSshCommand}";`; + } + command += `if ! git clone --branch ${customGitBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${customGitUrl} ${outputPath}; then + echo "❌ [ERROR] Fail to clone the repository ${customGitUrl}"; exit 1; fi - `, - ); - command.push(`echo "Cloned Custom Git ${customGitUrl}: βœ…" >> ${logPath};`); - return command.join("\n"); - } catch (error) { - throw error; - } + `; + + return command; }; const isHttpOrHttps = (url: string): boolean => { @@ -210,19 +90,19 @@ const isHttpOrHttps = (url: string): boolean => { return regex.test(url); }; -const addHostToKnownHosts = async (repositoryURL: string) => { - const { SSH_PATH } = paths(); - const { domain, port } = sanitizeRepoPathSSH(repositoryURL); - const knownHostsPath = path.join(SSH_PATH, "known_hosts"); +// const addHostToKnownHosts = async (repositoryURL: string) => { +// const { SSH_PATH } = paths(); +// const { domain, port } = sanitizeRepoPathSSH(repositoryURL); +// const knownHostsPath = path.join(SSH_PATH, "known_hosts"); - const command = `ssh-keyscan -p ${port} ${domain} >> ${knownHostsPath}`; - try { - await execAsync(command); - } catch (error) { - console.error(`Error adding host to known_hosts: ${error}`); - throw error; - } -}; +// const command = `ssh-keyscan -p ${port} ${domain} >> ${knownHostsPath}`; +// try { +// await execAsync(command); +// } catch (error) { +// console.error(`Error adding host to known_hosts: ${error}`); +// throw error; +// } +// }; const addHostToKnownHostsCommand = (repositoryURL: string) => { const { SSH_PATH } = paths(true); @@ -267,160 +147,43 @@ const sanitizeRepoPathSSH = (input: string) => { }; }; -export const cloneGitRawRepository = async (entity: { +interface Props { appName: string; - customGitUrl?: string | null; - customGitBranch?: string | null; - customGitSSHKeyId?: string | null; - enableSubmodules?: boolean; -}) => { - const { - appName, - customGitUrl, - customGitBranch, - customGitSSHKeyId, - enableSubmodules, - } = entity; + type?: "application" | "compose"; + serverId: string | null; +} - if (!customGitUrl || !customGitBranch) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error: Repository not found", - }); - } - - const { SSH_PATH, COMPOSE_PATH } = paths(); - const temporalKeyPath = path.join("/tmp", "id_rsa"); - const basePath = COMPOSE_PATH; +export const getGitCommitInfo = async ({ + appName, + type = "application", + serverId, +}: Props) => { + const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(!!serverId); + const basePath = type === "compose" ? COMPOSE_PATH : APPLICATIONS_PATH; const outputPath = join(basePath, appName, "code"); - const knownHostsPath = path.join(SSH_PATH, "known_hosts"); - - if (customGitSSHKeyId) { - const sshKey = await findSSHKeyById(customGitSSHKeyId); - - await execAsync(` - echo "${sshKey.privateKey}" > ${temporalKeyPath} - chmod 600 ${temporalKeyPath} - `); - } - + let stdoutResult = ""; + const result = { + message: "", + hash: "", + }; try { - if (!isHttpOrHttps(customGitUrl)) { - if (!customGitSSHKeyId) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: - "Error: you are trying to clone a ssh repository without a ssh key, please set a ssh key", - }); - } - await addHostToKnownHosts(customGitUrl); - } - await recreateDirectory(outputPath); - - if (customGitSSHKeyId) { - await updateSSHKeyById({ - sshKeyId: customGitSSHKeyId, - lastUsedAt: new Date().toISOString(), - }); + const gitCommand = `git -C ${outputPath} log -1 --pretty=format:"%H---DELIMITER---%B"`; + if (serverId) { + const { stdout } = await execAsyncRemote(serverId, gitCommand); + stdoutResult = stdout.trim(); + } else { + const { stdout } = await execAsync(gitCommand); + stdoutResult = stdout.trim(); } - const { port } = sanitizeRepoPathSSH(customGitUrl); - const cloneArgs = [ - "clone", - "--branch", - customGitBranch, - "--depth", - "1", - ...(enableSubmodules ? ["--recurse-submodules"] : []), - customGitUrl, - outputPath, - "--progress", - ]; - - await spawnAsync("git", cloneArgs, (_data) => {}, { - env: { - ...process.env, - ...(customGitSSHKeyId && { - GIT_SSH_COMMAND: `ssh -i ${temporalKeyPath}${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`, - }), - }, - }); + const parts = stdoutResult.split("---DELIMITER---"); + if (parts && parts.length === 2) { + result.hash = parts[0]?.trim() || ""; + result.message = parts[1]?.trim() || ""; + } } catch (error) { - throw error; - } -}; - -export const cloneRawGitRepositoryRemote = async (compose: Compose) => { - const { - appName, - customGitBranch, - customGitUrl, - customGitSSHKeyId, - serverId, - enableSubmodules, - } = compose; - - if (!serverId) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Server not found", - }); - } - if (!customGitUrl) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Git Provider not found", - }); - } - - const { SSH_PATH, COMPOSE_PATH } = paths(true); - const basePath = COMPOSE_PATH; - const outputPath = join(basePath, appName, "code"); - const knownHostsPath = path.join(SSH_PATH, "known_hosts"); - - if (customGitSSHKeyId) { - await updateSSHKeyById({ - sshKeyId: customGitSSHKeyId, - lastUsedAt: new Date().toISOString(), - }); - } - try { - const command = []; - if (!isHttpOrHttps(customGitUrl)) { - if (!customGitSSHKeyId) { - command.push( - `echo "Error: you are trying to clone a ssh repository without a ssh key, please set a ssh key ❌" ; - exit 1; - `, - ); - } - command.push(addHostToKnownHostsCommand(customGitUrl)); - } - command.push(`rm -rf ${outputPath};`); - command.push(`mkdir -p ${outputPath};`); - if (customGitSSHKeyId) { - const sshKey = await findSSHKeyById(customGitSSHKeyId); - const { port } = sanitizeRepoPathSSH(customGitUrl); - const gitSshCommand = `ssh -i /tmp/id_rsa${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`; - command.push( - ` - echo "${sshKey.privateKey}" > /tmp/id_rsa - chmod 600 /tmp/id_rsa - export GIT_SSH_COMMAND="${gitSshCommand}" - `, - ); - } - - command.push( - `if ! git clone --branch ${customGitBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${customGitUrl} ${outputPath} ; then - echo "[ERROR] Fail to clone the repository "; - exit 1; - fi - `, - ); - - await execAsyncRemote(serverId, command.join("\n")); - } catch (error) { - throw error; + console.error(`Error getting git commit info: ${error}`); + return null; } + return result; }; diff --git a/packages/server/src/utils/providers/gitea.ts b/packages/server/src/utils/providers/gitea.ts index db6dcbb78..ec8946ab3 100644 --- a/packages/server/src/utils/providers/gitea.ts +++ b/packages/server/src/utils/providers/gitea.ts @@ -1,7 +1,5 @@ -import { createWriteStream } from "node:fs"; import { join } from "node:path"; import { paths } from "@dokploy/server/constants"; -import type { Compose } from "@dokploy/server/services/compose"; import { findGiteaById, type Gitea, @@ -9,9 +7,6 @@ import { } from "@dokploy/server/services/gitea"; import type { InferResultType } from "@dokploy/server/types/with"; import { TRPCError } from "@trpc/server"; -import { recreateDirectory } from "../filesystem/directory"; -import { execAsyncRemote } from "../process/execAsync"; -import { spawnAsync } from "../process/spawnAsync"; export const getErrorCloneRequirements = (entity: { giteaRepository?: string | null; @@ -119,79 +114,27 @@ export type ApplicationWithGitea = InferResultType< export type ComposeWithGitea = InferResultType<"compose", { gitea: true }>; -export const getGiteaCloneCommand = async ( - entity: ApplicationWithGitea | ComposeWithGitea, - logPath: string, - isCompose = false, -) => { - const { - appName, - giteaBranch, - giteaId, - giteaOwner, - giteaRepository, - serverId, - enableSubmodules, - } = entity; - - if (!serverId) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Server not found", - }); - } - - if (!giteaId) { - const command = ` - echo "Error: ❌ Gitlab Provider not found" >> ${logPath}; - exit 1; - `; - - await execAsyncRemote(serverId, command); - throw new TRPCError({ - code: "NOT_FOUND", - message: "Gitea Provider not found", - }); - } - - // Use paths(true) for remote operations - const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(true); - await refreshGiteaToken(giteaId); - const gitea = await findGiteaById(giteaId); - const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH; - const outputPath = join(basePath, appName, "code"); - - const repoClone = `${giteaOwner}/${giteaRepository}.git`; - const cloneUrl = buildGiteaCloneUrl( - gitea?.giteaUrl!, - gitea?.accessToken!, - giteaOwner!, - giteaRepository!, - ); - - const cloneCommand = ` - rm -rf ${outputPath}; - mkdir -p ${outputPath}; - - if ! git clone --branch ${giteaBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then - echo "❌ [ERROR] Failed to clone the repository ${repoClone}" >> ${logPath}; - exit 1; - fi - - echo "Cloned ${repoClone} to ${outputPath}: βœ…" >> ${logPath}; - `; - - return cloneCommand; +type GiteaClone = (ApplicationWithGitea | ComposeWithGitea) & { + serverId: string | null; + type?: "application" | "compose"; }; -export const cloneGiteaRepository = async ( - entity: ApplicationWithGitea | ComposeWithGitea, - logPath: string, - isCompose = false, -) => { - const { APPLICATIONS_PATH, COMPOSE_PATH } = paths(); +interface CloneGiteaRepository { + appName: string; + giteaBranch: string | null; + giteaId: string | null; + giteaOwner: string | null; + giteaRepository: string | null; + enableSubmodules: boolean; + serverId: string | null; + type?: "application" | "compose"; +} - const writeStream = createWriteStream(logPath, { flags: "a" }); +export const cloneGiteaRepository = async ({ + type = "application", + ...entity +}: CloneGiteaRepository) => { + let command = "set -e;"; const { appName, giteaBranch, @@ -199,27 +142,27 @@ export const cloneGiteaRepository = async ( giteaOwner, giteaRepository, enableSubmodules, + serverId, } = entity; + const { APPLICATIONS_PATH, COMPOSE_PATH } = paths(!!serverId); if (!giteaId) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Gitea Provider not found", - }); + command += `echo "Error: ❌ Gitea Provider not found"; exit 1;`; + return command; } await refreshGiteaToken(giteaId); const giteaProvider = await findGiteaById(giteaId); + if (!giteaProvider) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Gitea provider not found in the database", - }); + command += `echo "❌ [ERROR] Gitea provider not found in the database"; exit 1;`; + return command; } - const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH; + const basePath = type === "compose" ? COMPOSE_PATH : APPLICATIONS_PATH; const outputPath = join(basePath, appName, "code"); - await recreateDirectory(outputPath); + command += `rm -rf ${outputPath};`; + command += `mkdir -p ${outputPath};`; const repoClone = `${giteaOwner}/${giteaRepository}.git`; const cloneUrl = buildGiteaCloneUrl( @@ -229,134 +172,9 @@ export const cloneGiteaRepository = async ( giteaRepository!, ); - writeStream.write(`\nCloning Repo ${repoClone} to ${outputPath}...\n`); - - try { - await spawnAsync( - "git", - [ - "clone", - "--branch", - giteaBranch!, - "--depth", - "1", - ...(enableSubmodules ? ["--recurse-submodules"] : []), - cloneUrl, - outputPath, - "--progress", - ], - (data) => { - if (writeStream.writable) { - writeStream.write(data); - } - }, - ); - writeStream.write(`\nCloned ${repoClone}: βœ…\n`); - } catch (error) { - writeStream.write(`ERROR Cloning: ${error}: ❌`); - throw error; - } finally { - writeStream.end(); - } -}; - -export const cloneRawGiteaRepository = async (entity: Compose) => { - const { - appName, - giteaRepository, - giteaOwner, - giteaBranch, - giteaId, - enableSubmodules, - } = entity; - const { COMPOSE_PATH } = paths(); - - if (!giteaId) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Gitea Provider not found", - }); - } - await refreshGiteaToken(giteaId); - const giteaProvider = await findGiteaById(giteaId); - if (!giteaProvider) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Gitea provider not found in the database", - }); - } - - const basePath = COMPOSE_PATH; - const outputPath = join(basePath, appName, "code"); - await recreateDirectory(outputPath); - - const cloneUrl = buildGiteaCloneUrl( - giteaProvider.giteaUrl, - giteaProvider.accessToken!, - giteaOwner!, - giteaRepository!, - ); - - try { - await spawnAsync("git", [ - "clone", - "--branch", - giteaBranch!, - "--depth", - "1", - ...(enableSubmodules ? ["--recurse-submodules"] : []), - cloneUrl, - outputPath, - "--progress", - ]); - } catch (error) { - throw error; - } -}; - -export const cloneRawGiteaRepositoryRemote = async (compose: Compose) => { - const { - appName, - giteaRepository, - giteaOwner, - giteaBranch, - giteaId, - serverId, - enableSubmodules, - } = compose; - - if (!serverId) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Server not found", - }); - } - if (!giteaId) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Gitea Provider not found", - }); - } - const { COMPOSE_PATH } = paths(true); - const giteaProvider = await findGiteaById(giteaId); - const basePath = COMPOSE_PATH; - const outputPath = join(basePath, appName, "code"); - const cloneUrl = buildGiteaCloneUrl( - giteaProvider.giteaUrl, - giteaProvider.accessToken!, - giteaOwner!, - giteaRepository!, - ); - - try { - const command = ` - rm -rf ${outputPath}; - git clone --branch ${giteaBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} - `; - await execAsyncRemote(serverId, command); - } catch (error) { - throw error; - } + command += `echo "Cloning Repo ${repoClone} to ${outputPath}: βœ…";`; + command += `git clone --branch ${giteaBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} --progress;`; + return command; }; export const haveGiteaRequirements = (giteaProvider: Gitea) => { diff --git a/packages/server/src/utils/providers/github.ts b/packages/server/src/utils/providers/github.ts index 30125db8b..5b7763df7 100644 --- a/packages/server/src/utils/providers/github.ts +++ b/packages/server/src/utils/providers/github.ts @@ -1,16 +1,11 @@ -import { createWriteStream } from "node:fs"; import { join } from "node:path"; import { paths } from "@dokploy/server/constants"; import type { apiFindGithubBranches } from "@dokploy/server/db/schema"; -import type { Compose } from "@dokploy/server/services/compose"; import { findGithubById, type Github } from "@dokploy/server/services/github"; import type { InferResultType } from "@dokploy/server/types/with"; import { createAppAuth } from "@octokit/auth-app"; import { TRPCError } from "@trpc/server"; import { Octokit } from "octokit"; -import { recreateDirectory } from "../filesystem/directory"; -import { execAsyncRemote } from "../process/execAsync"; -import { spawnAsync } from "../process/spawnAsync"; export const authGithub = (githubProvider: Github): Octokit => { if (!haveGithubRequirements(githubProvider)) { @@ -123,42 +118,39 @@ interface CloneGithubRepository { branch: string | null; githubId: string | null; repository: string | null; - logPath: string; type?: "application" | "compose"; enableSubmodules: boolean; + serverId: string | null; } export const cloneGithubRepository = async ({ - logPath, type = "application", ...entity }: CloneGithubRepository) => { + let command = "set -e;"; const isCompose = type === "compose"; - const { APPLICATIONS_PATH, COMPOSE_PATH } = paths(); - const writeStream = createWriteStream(logPath, { flags: "a" }); - const { appName, repository, owner, branch, githubId, enableSubmodules } = - entity; + const { + appName, + repository, + owner, + branch, + githubId, + enableSubmodules, + serverId, + } = entity; + const { APPLICATIONS_PATH, COMPOSE_PATH } = paths(!!serverId); if (!githubId) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "GitHub Provider not found", - }); + command += `echo "Error: ❌ Github Provider not found"; exit 1;`; + + return command; } const requirements = getErrorCloneRequirements(entity); // Check if requirements are met if (requirements.length > 0) { - writeStream.write( - `\nGitHub Repository configuration failed for application: ${appName}\n`, - ); - writeStream.write("Reasons:\n"); - writeStream.write(requirements.join("\n")); - writeStream.end(); - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error: GitHub repository information is incomplete.", - }); + command += `echo "GitHub Repository configuration failed for application: ${appName}"; echo "Reasons:"; echo "${requirements.join("\n")}"; exit 1;`; + return command; } const githubProvider = await findGithubById(githubId); @@ -167,193 +159,14 @@ export const cloneGithubRepository = async ({ const octokit = authGithub(githubProvider); const token = await getGithubToken(octokit); const repoclone = `github.com/${owner}/${repository}.git`; - await recreateDirectory(outputPath); + command += `rm -rf ${outputPath};`; + command += `mkdir -p ${outputPath};`; const cloneUrl = `https://oauth2:${token}@${repoclone}`; - try { - writeStream.write(`\nCloning Repo ${repoclone} to ${outputPath}: βœ…\n`); - const cloneArgs = [ - "clone", - "--branch", - branch!, - "--depth", - "1", - ...(enableSubmodules ? ["--recurse-submodules"] : []), - cloneUrl, - outputPath, - "--progress", - ]; + command += `echo "Cloning Repo ${repoclone} to ${outputPath}: βœ…";`; + command += `git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} --progress;`; - await spawnAsync("git", cloneArgs, (data) => { - if (writeStream.writable) { - writeStream.write(data); - } - }); - writeStream.write(`\nCloned ${repoclone}: βœ…\n`); - } catch (error) { - writeStream.write(`ERROR Cloning: ${error}: ❌`); - throw error; - } finally { - writeStream.end(); - } -}; - -export const getGithubCloneCommand = async ({ - logPath, - type = "application", - ...entity -}: CloneGithubRepository & { serverId: string }) => { - const { - appName, - repository, - owner, - branch, - githubId, - serverId, - enableSubmodules, - } = entity; - const isCompose = type === "compose"; - if (!serverId) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Server not found", - }); - } - - if (!githubId) { - const command = ` - echo "Error: ❌ Github Provider not found" >> ${logPath}; - exit 1; - `; - - await execAsyncRemote(serverId, command); - throw new TRPCError({ - code: "NOT_FOUND", - message: "GitHub Provider not found", - }); - } - - const requirements = getErrorCloneRequirements(entity); - - // Build log messages - let logMessages = ""; - if (requirements.length > 0) { - logMessages += `\nGitHub Repository configuration failed for application: ${appName}\n`; - logMessages += "Reasons:\n"; - logMessages += requirements.join("\n"); - const escapedLogMessages = logMessages - .replace(/\\/g, "\\\\") - .replace(/"/g, '\\"') - .replace(/\n/g, "\\n"); - - const bashCommand = ` - echo "${escapedLogMessages}" >> ${logPath}; - exit 1; # Exit with error code - `; - - await execAsyncRemote(serverId, bashCommand); - return; - } - const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(true); - const githubProvider = await findGithubById(githubId); - const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH; - const outputPath = join(basePath, appName, "code"); - const octokit = authGithub(githubProvider); - const token = await getGithubToken(octokit); - const repoclone = `github.com/${owner}/${repository}.git`; - const cloneUrl = `https://oauth2:${token}@${repoclone}`; - - const cloneCommand = ` -rm -rf ${outputPath}; -mkdir -p ${outputPath}; -if ! git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then - echo "❌ [ERROR] Fail to clone repository ${repoclone}" >> ${logPath}; - exit 1; -fi -echo "Cloned ${repoclone} to ${outputPath}: βœ…" >> ${logPath}; - `; - - return cloneCommand; -}; - -export const cloneRawGithubRepository = async (entity: Compose) => { - const { appName, repository, owner, branch, githubId, enableSubmodules } = - entity; - - if (!githubId) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "GitHub Provider not found", - }); - } - const { COMPOSE_PATH } = paths(); - const githubProvider = await findGithubById(githubId); - const basePath = COMPOSE_PATH; - const outputPath = join(basePath, appName, "code"); - const octokit = authGithub(githubProvider); - const token = await getGithubToken(octokit); - const repoclone = `github.com/${owner}/${repository}.git`; - await recreateDirectory(outputPath); - const cloneUrl = `https://oauth2:${token}@${repoclone}`; - try { - const cloneArgs = [ - "clone", - "--branch", - branch!, - "--depth", - "1", - ...(enableSubmodules ? ["--recurse-submodules"] : []), - cloneUrl, - outputPath, - "--progress", - ]; - await spawnAsync("git", cloneArgs); - } catch (error) { - throw error; - } -}; - -export const cloneRawGithubRepositoryRemote = async (compose: Compose) => { - const { - appName, - repository, - owner, - branch, - githubId, - serverId, - enableSubmodules, - } = compose; - - if (!serverId) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Server not found", - }); - } - if (!githubId) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "GitHub Provider not found", - }); - } - - const { COMPOSE_PATH } = paths(true); - const githubProvider = await findGithubById(githubId); - const basePath = COMPOSE_PATH; - const outputPath = join(basePath, appName, "code"); - const octokit = authGithub(githubProvider); - const token = await getGithubToken(octokit); - const repoclone = `github.com/${owner}/${repository}.git`; - const cloneUrl = `https://oauth2:${token}@${repoclone}`; - try { - const command = ` - rm -rf ${outputPath}; - git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} - `; - await execAsyncRemote(serverId, command); - } catch (error) { - throw error; - } + return command; }; export const getGithubRepositories = async (githubId?: string) => { diff --git a/packages/server/src/utils/providers/gitlab.ts b/packages/server/src/utils/providers/gitlab.ts index 840347fdb..3343c9bb6 100644 --- a/packages/server/src/utils/providers/gitlab.ts +++ b/packages/server/src/utils/providers/gitlab.ts @@ -1,8 +1,6 @@ -import { createWriteStream } from "node:fs"; import { join } from "node:path"; import { paths } from "@dokploy/server/constants"; import type { apiGitlabTestConnection } from "@dokploy/server/db/schema"; -import type { Compose } from "@dokploy/server/services/compose"; import { findGitlabById, type Gitlab, @@ -10,9 +8,6 @@ import { } from "@dokploy/server/services/gitlab"; import type { InferResultType } from "@dokploy/server/types/with"; import { TRPCError } from "@trpc/server"; -import { recreateDirectory } from "../filesystem/directory"; -import { execAsyncRemote } from "../process/execAsync"; -import { spawnAsync } from "../process/spawnAsync"; export const refreshGitlabToken = async (gitlabProviderId: string) => { const gitlabProvider = await findGitlabById(gitlabProviderId); @@ -102,25 +97,34 @@ const getGitlabCloneUrl = (gitlab: GitlabInfo, repoClone: string) => { return cloneUrl; }; -export const cloneGitlabRepository = async ( - entity: ApplicationWithGitlab | ComposeWithGitlab, - logPath: string, - isCompose = false, -) => { - const writeStream = createWriteStream(logPath, { flags: "a" }); +interface CloneGitlabRepository { + appName: string; + gitlabBranch: string | null; + gitlabId: string | null; + gitlabPathNamespace: string | null; + enableSubmodules: boolean; + serverId: string | null; + type?: "application" | "compose"; +} + +export const cloneGitlabRepository = async ({ + type = "application", + ...entity +}: CloneGitlabRepository) => { + let command = "set -e;"; const { appName, gitlabBranch, gitlabId, gitlabPathNamespace, enableSubmodules, + serverId, } = entity; + const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(!!serverId); if (!gitlabId) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Gitlab Provider not found", - }); + command += `echo "Error: ❌ Gitlab Provider not found"; exit 1;`; + return command; } await refreshGitlabToken(gitlabId); @@ -130,127 +134,19 @@ export const cloneGitlabRepository = async ( // Check if requirements are met if (requirements.length > 0) { - writeStream.write( - `\nGitLab Repository configuration failed for application: ${appName}\n`, - ); - writeStream.write("Reasons:\n"); - writeStream.write(requirements.join("\n")); - writeStream.end(); - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error: GitLab repository information is incomplete.", - }); + command += `echo "❌ [ERROR] GitLab Repository configuration failed for application: ${appName}"; echo "Reasons:"; echo "${requirements.join("\n")}"; exit 1;`; + return command; } - const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(); - const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH; + const basePath = type === "compose" ? COMPOSE_PATH : APPLICATIONS_PATH; const outputPath = join(basePath, appName, "code"); - await recreateDirectory(outputPath); + command += `rm -rf ${outputPath};`; + command += `mkdir -p ${outputPath};`; const repoClone = getGitlabRepoClone(gitlab, gitlabPathNamespace); const cloneUrl = getGitlabCloneUrl(gitlab, repoClone); - try { - writeStream.write(`\nCloning Repo ${repoClone} to ${outputPath}: βœ…\n`); - const cloneArgs = [ - "clone", - "--branch", - gitlabBranch!, - "--depth", - "1", - ...(enableSubmodules ? ["--recurse-submodules"] : []), - cloneUrl, - outputPath, - "--progress", - ]; - - await spawnAsync("git", cloneArgs, (data) => { - if (writeStream.writable) { - writeStream.write(data); - } - }); - writeStream.write(`\nCloned ${repoClone}: βœ…\n`); - } catch (error) { - writeStream.write(`ERROR Cloning: ${error}: ❌`); - throw error; - } finally { - writeStream.end(); - } -}; - -export const getGitlabCloneCommand = async ( - entity: ApplicationWithGitlab | ComposeWithGitlab, - logPath: string, - isCompose = false, -) => { - const { - appName, - gitlabPathNamespace, - gitlabBranch, - gitlabId, - serverId, - enableSubmodules, - } = entity; - - if (!serverId) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Server not found", - }); - } - - if (!gitlabId) { - const command = ` - echo "Error: ❌ Gitlab Provider not found" >> ${logPath}; - exit 1; - `; - - await execAsyncRemote(serverId, command); - throw new TRPCError({ - code: "NOT_FOUND", - message: "Gitlab Provider not found", - }); - } - - const requirements = getErrorCloneRequirements(entity); - - // Build log messages - let logMessages = ""; - if (requirements.length > 0) { - logMessages += `\nGitLab Repository configuration failed for application: ${appName}\n`; - logMessages += "Reasons:\n"; - logMessages += requirements.join("\n"); - const escapedLogMessages = logMessages - .replace(/\\/g, "\\\\") - .replace(/"/g, '\\"') - .replace(/\n/g, "\\n"); - - const bashCommand = ` - echo "${escapedLogMessages}" >> ${logPath}; - exit 1; # Exit with error code - `; - - await execAsyncRemote(serverId, bashCommand); - return; - } - - const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(true); - await refreshGitlabToken(gitlabId); - const gitlab = await findGitlabById(gitlabId); - const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH; - const outputPath = join(basePath, appName, "code"); - await recreateDirectory(outputPath); - const repoClone = getGitlabRepoClone(gitlab, gitlabPathNamespace); - const cloneUrl = getGitlabCloneUrl(gitlab, repoClone); - const cloneCommand = ` -rm -rf ${outputPath}; -mkdir -p ${outputPath}; -if ! git clone --branch ${gitlabBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then - echo "❌ [ERROR] Fail to clone the repository ${repoClone}" >> ${logPath}; - exit 1; -fi -echo "Cloned ${repoClone} to ${outputPath}: βœ…" >> ${logPath}; - `; - - return cloneCommand; + command += `echo "Cloning Repo ${repoClone} to ${outputPath}: βœ…";`; + command += `git clone --branch ${gitlabBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} --progress;`; + return command; }; export const getGitlabRepositories = async (gitlabId?: string) => { @@ -355,88 +251,6 @@ export const getGitlabBranches = async (input: { }[]; }; -export const cloneRawGitlabRepository = async (entity: Compose) => { - const { - appName, - gitlabBranch, - gitlabId, - gitlabPathNamespace, - enableSubmodules, - } = entity; - - if (!gitlabId) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Gitlab Provider not found", - }); - } - - const { COMPOSE_PATH } = paths(); - await refreshGitlabToken(gitlabId); - const gitlabProvider = await findGitlabById(gitlabId); - const basePath = COMPOSE_PATH; - const outputPath = join(basePath, appName, "code"); - await recreateDirectory(outputPath); - const repoClone = getGitlabRepoClone(gitlabProvider, gitlabPathNamespace); - const cloneUrl = getGitlabCloneUrl(gitlabProvider, repoClone); - try { - const cloneArgs = [ - "clone", - "--branch", - gitlabBranch!, - "--depth", - "1", - ...(enableSubmodules ? ["--recurse-submodules"] : []), - cloneUrl, - outputPath, - "--progress", - ]; - await spawnAsync("git", cloneArgs); - } catch (error) { - throw error; - } -}; - -export const cloneRawGitlabRepositoryRemote = async (compose: Compose) => { - const { - appName, - gitlabPathNamespace, - gitlabBranch, - gitlabId, - serverId, - enableSubmodules, - } = compose; - - if (!serverId) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Server not found", - }); - } - if (!gitlabId) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Gitlab Provider not found", - }); - } - const { COMPOSE_PATH } = paths(true); - await refreshGitlabToken(gitlabId); - const gitlabProvider = await findGitlabById(gitlabId); - const basePath = COMPOSE_PATH; - const outputPath = join(basePath, appName, "code"); - const repoClone = getGitlabRepoClone(gitlabProvider, gitlabPathNamespace); - const cloneUrl = getGitlabCloneUrl(gitlabProvider, repoClone); - try { - const command = ` - rm -rf ${outputPath}; - git clone --branch ${gitlabBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} - `; - await execAsyncRemote(serverId, command); - } catch (error) { - throw error; - } -}; - export const testGitlabConnection = async ( input: typeof apiGitlabTestConnection._type, ) => { @@ -476,7 +290,7 @@ export const validateGitlabProvider = async (gitlabProvider: Gitlab) => { while (true) { 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: { Authorization: `Bearer ${gitlabProvider.accessToken}`, diff --git a/packages/server/src/utils/providers/raw.ts b/packages/server/src/utils/providers/raw.ts index 34ba0012a..508df86ed 100644 --- a/packages/server/src/utils/providers/raw.ts +++ b/packages/server/src/utils/providers/raw.ts @@ -1,40 +1,10 @@ -import { createWriteStream } from "node:fs"; -import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { paths } from "@dokploy/server/constants"; import type { Compose } from "@dokploy/server/services/compose"; import { encodeBase64 } from "../docker/utils"; -import { recreateDirectory } from "../filesystem/directory"; -import { execAsyncRemote } from "../process/execAsync"; -export const createComposeFile = async (compose: Compose, logPath: string) => { - const { COMPOSE_PATH } = paths(); - const { appName, composeFile } = compose; - const writeStream = createWriteStream(logPath, { flags: "a" }); - const outputPath = join(COMPOSE_PATH, appName, "code"); - - try { - await recreateDirectory(outputPath); - writeStream.write( - `\nCreating File 'docker-compose.yml' to ${outputPath}: βœ…\n`, - ); - - await writeFile(join(outputPath, "docker-compose.yml"), composeFile); - - writeStream.write(`\nFile 'docker-compose.yml' created: βœ…\n`); - } catch (error) { - writeStream.write(`\nERROR Creating Compose File: ${error}: ❌\n`); - throw error; - } finally { - writeStream.end(); - } -}; - -export const getCreateComposeFileCommand = ( - compose: Compose, - logPath: string, -) => { - const { COMPOSE_PATH } = paths(true); +export const getCreateComposeFileCommand = (compose: Compose) => { + const { COMPOSE_PATH } = paths(!!compose.serverId); const { appName, composeFile } = compose; const outputPath = join(COMPOSE_PATH, appName, "code"); const filePath = join(outputPath, "docker-compose.yml"); @@ -43,39 +13,7 @@ export const getCreateComposeFileCommand = ( rm -rf ${outputPath}; mkdir -p ${outputPath}; echo "${encodedContent}" | base64 -d > "${filePath}"; - echo "File 'docker-compose.yml' created: βœ…" >> ${logPath}; + echo "File 'docker-compose.yml' created: βœ…"; `; return bashCommand; }; - -export const createComposeFileRaw = async (compose: Compose) => { - const { COMPOSE_PATH } = paths(); - const { appName, composeFile } = compose; - const outputPath = join(COMPOSE_PATH, appName, "code"); - const filePath = join(outputPath, "docker-compose.yml"); - try { - await recreateDirectory(outputPath); - await writeFile(filePath, composeFile); - } catch (error) { - throw error; - } -}; - -export const createComposeFileRawRemote = async (compose: Compose) => { - const { COMPOSE_PATH } = paths(true); - const { appName, composeFile, serverId } = compose; - const outputPath = join(COMPOSE_PATH, appName, "code"); - const filePath = join(outputPath, "docker-compose.yml"); - - try { - const encodedContent = encodeBase64(composeFile); - const command = ` - rm -rf ${outputPath}; - mkdir -p ${outputPath}; - echo "${encodedContent}" | base64 -d > "${filePath}"; - `; - await execAsyncRemote(serverId, command); - } catch (error) { - throw error; - } -}; diff --git a/packages/server/src/utils/restore/utils.ts b/packages/server/src/utils/restore/utils.ts index c46077238..23052e642 100644 --- a/packages/server/src/utils/restore/utils.ts +++ b/packages/server/src/utils/restore/utils.ts @@ -7,7 +7,7 @@ export const getPostgresRestoreCommand = ( database: string, databaseUser: string, ) => { - return `docker exec -i $CONTAINER_ID sh -c "pg_restore -U ${databaseUser} -d ${database} -O --clean --if-exists"`; + return `docker exec -i $CONTAINER_ID sh -c "pg_restore -U '${databaseUser}' -d ${database} -O --clean --if-exists"`; }; export const getMariadbRestoreCommand = ( @@ -15,14 +15,14 @@ export const getMariadbRestoreCommand = ( databaseUser: string, databasePassword: string, ) => { - return `docker exec -i $CONTAINER_ID sh -c "mariadb -u ${databaseUser} -p${databasePassword} ${database}"`; + return `docker exec -i $CONTAINER_ID sh -c "mariadb -u '${databaseUser}' -p'${databasePassword}' ${database}"`; }; export const getMysqlRestoreCommand = ( database: string, databasePassword: string, ) => { - return `docker exec -i $CONTAINER_ID sh -c "mysql -u root -p${databasePassword} ${database}"`; + return `docker exec -i $CONTAINER_ID sh -c "mysql -u root -p'${databasePassword}' ${database}"`; }; export const getMongoRestoreCommand = ( @@ -30,7 +30,7 @@ export const getMongoRestoreCommand = ( databaseUser: string, databasePassword: string, ) => { - return `docker exec -i $CONTAINER_ID sh -c "mongorestore --username ${databaseUser} --password ${databasePassword} --authenticationDatabase admin --db ${database} --archive"`; + return `docker exec -i $CONTAINER_ID sh -c "mongorestore --username '${databaseUser}' --password '${databasePassword}' --authenticationDatabase admin --db ${database} --archive"`; }; export const getComposeSearchCommand = ( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1aae074f7..a03f77f4c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -347,8 +347,8 @@ importers: specifier: ^0.2.1 version: 0.2.1(next@15.3.2(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) node-os-utils: - specifier: 1.3.7 - version: 1.3.7 + specifier: 2.0.1 + version: 2.0.1 node-pty: specifier: 1.0.0 version: 1.0.0 @@ -406,6 +406,9 @@ importers: rotating-file-stream: specifier: 3.2.3 version: 3.2.3 + shell-quote: + specifier: ^1.8.1 + version: 1.8.2 slugify: specifier: ^1.6.6 version: 1.6.6 @@ -473,9 +476,6 @@ importers: '@types/node': specifier: ^18.19.104 version: 18.19.104 - '@types/node-os-utils': - specifier: 1.3.4 - version: 1.3.4 '@types/node-schedule': specifier: 2.1.6 version: 2.1.6 @@ -491,6 +491,9 @@ importers: '@types/react-dom': specifier: 18.3.0 version: 18.3.0 + '@types/shell-quote': + specifier: ^1.7.5 + version: 1.7.5 '@types/ssh2': specifier: 1.15.1 version: 1.15.1 @@ -688,8 +691,8 @@ importers: specifier: 3.3.11 version: 3.3.11 node-os-utils: - specifier: 1.3.7 - version: 1.3.7 + specifier: 2.0.1 + version: 2.0.1 node-pty: specifier: 1.0.0 version: 1.0.0 @@ -729,6 +732,9 @@ importers: rotating-file-stream: specifier: 3.2.3 version: 3.2.3 + shell-quote: + specifier: ^1.8.1 + version: 1.8.2 slugify: specifier: ^1.6.6 version: 1.6.6 @@ -766,9 +772,6 @@ importers: '@types/node': specifier: ^18.19.104 version: 18.19.104 - '@types/node-os-utils': - specifier: 1.3.4 - version: 1.3.4 '@types/node-schedule': specifier: 2.1.6 version: 2.1.6 @@ -784,6 +787,9 @@ importers: '@types/react-dom': specifier: 18.3.0 version: 18.3.0 + '@types/shell-quote': + specifier: ^1.7.5 + version: 1.7.5 '@types/ssh2': specifier: 1.15.1 version: 1.15.1 @@ -4000,9 +4006,6 @@ packages: '@types/mysql@2.15.26': resolution: {integrity: sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==} - '@types/node-os-utils@1.3.4': - resolution: {integrity: sha512-BCUYrbdoO4FUbx6MB9atLNFnkxdliFaxdiTJMIPPiecXIApc5zf4NIqV5G1jWv/ReZvtYyHLs40RkBjHX+vykA==} - '@types/node-schedule@2.1.6': resolution: {integrity: sha512-6AlZSUiNTdaVmH5jXYxX9YgmF1zfOlbjUqw0EllTBmZCnN1R5RR/m/u3No1OiWR05bnQ4jM4/+w4FcGvkAtnKQ==} @@ -4042,6 +4045,9 @@ packages: '@types/readable-stream@4.0.20': resolution: {integrity: sha512-eLgbR5KwUh8+6pngBDxS32MymdCsCHnGtwHTrC0GDorbc7NbcnkZAWptDLgZiRk9VRas+B6TyRgPDucq4zRs8g==} + '@types/shell-quote@1.7.5': + resolution: {integrity: sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw==} + '@types/shimmer@1.2.0': resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==} @@ -6312,8 +6318,9 @@ packages: '@types/node': optional: true - node-os-utils@1.3.7: - resolution: {integrity: sha512-fvnX9tZbR7WfCG5BAy3yO/nCLyjVWD6MghEq0z5FDfN+ZXpLWNITBdbifxQkQ25ebr16G0N7eRWJisOcMEHG3Q==} + node-os-utils@2.0.1: + resolution: {integrity: sha512-rH2N3qHZETLhdgTGhMMCE8zU3gsWO4we1MFtrSiAI7tYWrnJRc6dk2PseV4co3Lb0v/MbRONLQI2biHQYbpTpg==} + engines: {node: '>=18.0.0'} node-pty@1.0.0: resolution: {integrity: sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==} @@ -11338,8 +11345,6 @@ snapshots: dependencies: '@types/node': 20.17.51 - '@types/node-os-utils@1.3.4': {} - '@types/node-schedule@2.1.6': dependencies: '@types/node': 20.17.51 @@ -11393,6 +11398,8 @@ snapshots: dependencies: '@types/node': 20.17.51 + '@types/shell-quote@1.7.5': {} + '@types/shimmer@1.2.0': {} '@types/ssh2@1.15.1': @@ -13852,7 +13859,7 @@ snapshots: optionalDependencies: '@types/node': 18.19.104 - node-os-utils@1.3.7: {} + node-os-utils@2.0.1: {} node-pty@1.0.0: dependencies: