mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-18 05:35:26 +02:00
Compare commits
126 Commits
v0.25.8
...
2326-add-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e052850b87 | ||
|
|
e06f5979c3 | ||
|
|
6b346d30ee | ||
|
|
9e98f9ce7f | ||
|
|
c8e7aae5c6 | ||
|
|
75a49790ea | ||
|
|
716e8b351f | ||
|
|
e993955f5a | ||
|
|
caf0aa6a12 | ||
|
|
21eb185431 | ||
|
|
bb3f73851a | ||
|
|
40949f2a8f | ||
|
|
fe7a73baee | ||
|
|
b1505651c2 | ||
|
|
689c689487 | ||
|
|
1aac5c1670 | ||
|
|
ea83406f6f | ||
|
|
25aecab062 | ||
|
|
9e11b802fd | ||
|
|
adfe29e10c | ||
|
|
c1d23b18fb | ||
|
|
272a8dbdb2 | ||
|
|
dc4e8ecdc9 | ||
|
|
559753eae3 | ||
|
|
2d0669e288 | ||
|
|
3f12f20e4c | ||
|
|
4907a021a4 | ||
|
|
817825e8bd | ||
|
|
0f632e3f55 | ||
|
|
8728d4b600 | ||
|
|
88b4374019 | ||
|
|
b91cb6cb5e | ||
|
|
c8277f6573 | ||
|
|
24c216e61a | ||
|
|
5c630e7ad7 | ||
|
|
c0dec0ed20 | ||
|
|
7d9806a050 | ||
|
|
96e7b39e3c | ||
|
|
ded16f39af | ||
|
|
d8e521e4dc | ||
|
|
67643fe088 | ||
|
|
aab982b431 | ||
|
|
362416afa8 | ||
|
|
035f8835cf | ||
|
|
8cff84ef54 | ||
|
|
742ca00d3d | ||
|
|
3481da9b0e | ||
|
|
15634c9f10 | ||
|
|
704582f6de | ||
|
|
65d962efc8 | ||
|
|
78d2e13dc8 | ||
|
|
28f7fb90c0 | ||
|
|
8647e7a6b7 | ||
|
|
cc1620b5fa | ||
|
|
27b605f961 | ||
|
|
a72281c018 | ||
|
|
aa750be036 | ||
|
|
067777f28e | ||
|
|
f77a67ba33 | ||
|
|
30d2f38259 | ||
|
|
b23ba17a41 | ||
|
|
218c077255 | ||
|
|
f94d5b9582 | ||
|
|
b9d05b00a9 | ||
|
|
f61fb3aba0 | ||
|
|
d3b7e68da9 | ||
|
|
061ca6c95c | ||
|
|
e576c1a63f | ||
|
|
5d53cf4090 | ||
|
|
ff27f0828b | ||
|
|
33d4f57611 | ||
|
|
bacadccaa9 | ||
|
|
55748749fd | ||
|
|
45b75fdfde | ||
|
|
ff822481c5 | ||
|
|
783324628f | ||
|
|
e70c476c9f | ||
|
|
891260fe41 | ||
|
|
062037a9e6 | ||
|
|
7da1be877b | ||
|
|
60e6285e8e | ||
|
|
cd8c67bb9b | ||
|
|
4fb3ad3032 | ||
|
|
736a7320d4 | ||
|
|
23b235303c | ||
|
|
eb8c6e4367 | ||
|
|
965f05c7c8 | ||
|
|
e316beaddb | ||
|
|
8aff1e7614 | ||
|
|
dbe1733dcb | ||
|
|
73d87c06e1 | ||
|
|
e136934cbc | ||
|
|
4840abe3a4 | ||
|
|
f046ba427a | ||
|
|
b12e84c645 | ||
|
|
d18fe8390b | ||
|
|
e88a9ce96f | ||
|
|
1c652477fb | ||
|
|
a5abd46386 | ||
|
|
ad0e044740 | ||
|
|
7a0ff72f51 | ||
|
|
2e702dc41f | ||
|
|
766f9244da | ||
|
|
6413fa54e6 | ||
|
|
1c9dcc0c9e | ||
|
|
fee802a57b | ||
|
|
af2b053caa | ||
|
|
42a4cc7fff | ||
|
|
2a7807c2b3 | ||
|
|
153390ff26 | ||
|
|
425b8ec3c2 | ||
|
|
e86caccfd5 | ||
|
|
8a93116ce0 | ||
|
|
daff2adb02 | ||
|
|
052fc5ffe1 | ||
|
|
a79afe49b4 | ||
|
|
48503c96c1 | ||
|
|
d08fdeb939 | ||
|
|
8ca8839d7e | ||
|
|
8b13919d3b | ||
|
|
b2264a9148 | ||
|
|
f7ddc715c7 | ||
|
|
63568a4887 | ||
|
|
e4aefe7f9d | ||
|
|
15c81a0982 | ||
|
|
4b44bc86b4 |
26
.github/workflows/pull-request.yml
vendored
26
.github/workflows/pull-request.yml
vendored
@@ -20,6 +20,32 @@ jobs:
|
||||
with:
|
||||
node-version: 20.16.0
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install Nixpacks
|
||||
if: matrix.job == 'test'
|
||||
run: |
|
||||
export NIXPACKS_VERSION=1.39.0
|
||||
curl -sSL https://nixpacks.com/install.sh | bash
|
||||
echo "Nixpacks installed $NIXPACKS_VERSION"
|
||||
|
||||
- name: Install Railpack
|
||||
if: matrix.job == 'test'
|
||||
run: |
|
||||
export RAILPACK_VERSION=0.15.0
|
||||
curl -sSL https://railpack.com/install.sh | bash
|
||||
echo "Railpack installed $RAILPACK_VERSION"
|
||||
|
||||
- name: Add build tools to PATH
|
||||
if: matrix.job == 'test'
|
||||
run: echo "$HOME/.local/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Initialize Docker Swarm
|
||||
if: matrix.job == 'test'
|
||||
run: |
|
||||
docker swarm init
|
||||
docker network create --driver overlay dokploy-network || true
|
||||
echo "✅ Docker Swarm initialized"
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm server:build
|
||||
- run: pnpm ${{ matrix.job }}
|
||||
|
||||
70
.github/workflows/sync-openapi-docs.yml
vendored
Normal file
70
.github/workflows/sync-openapi-docs.yml
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
name: Generate and Sync OpenAPI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- canary
|
||||
- main
|
||||
paths:
|
||||
- 'apps/dokploy/server/api/routers/**'
|
||||
- 'packages/server/src/services/**'
|
||||
- 'packages/server/src/db/schema/**'
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
generate-and-commit:
|
||||
name: Generate OpenAPI and commit to Dokploy repo
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Dokploy repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.16.0
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Generate OpenAPI specification
|
||||
run: |
|
||||
pnpm generate:openapi
|
||||
|
||||
# Verifica que se generó correctamente
|
||||
if [ ! -f openapi.json ]; then
|
||||
echo "❌ openapi.json not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ OpenAPI specification generated successfully"
|
||||
|
||||
- name: Sync to website repository
|
||||
run: |
|
||||
# Clona el repositorio de website
|
||||
git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/website.git website-repo
|
||||
|
||||
cd website-repo
|
||||
|
||||
# Copia el openapi.json al website (sobrescribe)
|
||||
mkdir -p apps/docs/public
|
||||
cp -f ../openapi.json apps/docs/public/openapi.json
|
||||
|
||||
# Configura git
|
||||
git config user.name "Dokploy Bot"
|
||||
git config user.email "bot@dokploy.com"
|
||||
|
||||
# Agrega y commitea siempre
|
||||
git add apps/docs/public/openapi.json
|
||||
git commit -m "chore: sync OpenAPI specification [skip ci]" \
|
||||
-m "Source: ${{ github.repository }}@${{ github.sha }}" \
|
||||
-m "Updated: $(date -u +'%Y-%m-%d %H:%M:%S UTC')" \
|
||||
--allow-empty
|
||||
|
||||
git push
|
||||
|
||||
echo "✅ OpenAPI synced to website successfully"
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -13,6 +13,8 @@ node_modules
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
openapi.json
|
||||
|
||||
# Testing
|
||||
coverage
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
DATABASE_URL="postgres://dokploy:amukds4wi9001583845717ad2@localhost:5432/dokploy"
|
||||
PORT=3000
|
||||
NODE_ENV=development
|
||||
NODE_ENV=development
|
||||
|
||||
276
apps/dokploy/__test__/deploy/application.command.test.ts
Normal file
276
apps/dokploy/__test__/deploy/application.command.test.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
import * as adminService from "@dokploy/server/services/admin";
|
||||
import * as applicationService from "@dokploy/server/services/application";
|
||||
import { deployApplication } from "@dokploy/server/services/application";
|
||||
import * as deploymentService from "@dokploy/server/services/deployment";
|
||||
import * as builders from "@dokploy/server/utils/builders";
|
||||
import * as notifications from "@dokploy/server/utils/notifications/build-success";
|
||||
import * as execProcess from "@dokploy/server/utils/process/execAsync";
|
||||
import * as gitProvider from "@dokploy/server/utils/providers/git";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("@dokploy/server/db", () => {
|
||||
const createChainableMock = (): any => {
|
||||
const chain = {
|
||||
set: vi.fn(() => chain),
|
||||
where: vi.fn(() => chain),
|
||||
returning: vi.fn().mockResolvedValue([{}] as any),
|
||||
} as any;
|
||||
return chain;
|
||||
};
|
||||
|
||||
return {
|
||||
db: {
|
||||
select: vi.fn(),
|
||||
insert: vi.fn(),
|
||||
update: vi.fn(() => createChainableMock()),
|
||||
delete: vi.fn(),
|
||||
query: {
|
||||
applications: {
|
||||
findFirst: vi.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@dokploy/server/services/application", async () => {
|
||||
const actual = await vi.importActual<
|
||||
typeof import("@dokploy/server/services/application")
|
||||
>("@dokploy/server/services/application");
|
||||
return {
|
||||
...actual,
|
||||
findApplicationById: vi.fn(),
|
||||
updateApplicationStatus: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@dokploy/server/services/admin", () => ({
|
||||
getDokployUrl: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@dokploy/server/services/deployment", () => ({
|
||||
createDeployment: vi.fn(),
|
||||
updateDeploymentStatus: vi.fn(),
|
||||
updateDeployment: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@dokploy/server/utils/providers/git", async () => {
|
||||
const actual = await vi.importActual<
|
||||
typeof import("@dokploy/server/utils/providers/git")
|
||||
>("@dokploy/server/utils/providers/git");
|
||||
return {
|
||||
...actual,
|
||||
getGitCommitInfo: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@dokploy/server/utils/process/execAsync", () => ({
|
||||
execAsync: vi.fn(),
|
||||
ExecError: class ExecError extends Error {},
|
||||
}));
|
||||
|
||||
vi.mock("@dokploy/server/utils/builders", async () => {
|
||||
const actual = await vi.importActual<
|
||||
typeof import("@dokploy/server/utils/builders")
|
||||
>("@dokploy/server/utils/builders");
|
||||
return {
|
||||
...actual,
|
||||
mechanizeDockerContainer: vi.fn(),
|
||||
getBuildCommand: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@dokploy/server/utils/notifications/build-success", () => ({
|
||||
sendBuildSuccessNotifications: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@dokploy/server/utils/notifications/build-error", () => ({
|
||||
sendBuildErrorNotifications: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@dokploy/server/services/rollbacks", () => ({
|
||||
createRollback: vi.fn(),
|
||||
}));
|
||||
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { cloneGitRepository } from "@dokploy/server/utils/providers/git";
|
||||
|
||||
const createMockApplication = (overrides = {}) => ({
|
||||
applicationId: "test-app-id",
|
||||
name: "Test App",
|
||||
appName: "test-app",
|
||||
sourceType: "git" as const,
|
||||
customGitUrl: "https://github.com/Dokploy/examples.git",
|
||||
customGitBranch: "main",
|
||||
customGitSSHKeyId: null,
|
||||
buildType: "nixpacks" as const,
|
||||
buildPath: "/astro",
|
||||
env: "NODE_ENV=production",
|
||||
serverId: null,
|
||||
rollbackActive: false,
|
||||
enableSubmodules: false,
|
||||
environmentId: "env-id",
|
||||
environment: {
|
||||
projectId: "project-id",
|
||||
env: "",
|
||||
name: "production",
|
||||
project: {
|
||||
name: "Test Project",
|
||||
organizationId: "org-id",
|
||||
env: "",
|
||||
},
|
||||
},
|
||||
domains: [],
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const createMockDeployment = () => ({
|
||||
deploymentId: "deployment-id",
|
||||
logPath: "/tmp/test-deployment.log",
|
||||
});
|
||||
|
||||
describe("deployApplication - Command Generation Tests", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(db.query.applications.findFirst).mockResolvedValue(
|
||||
createMockApplication() as any,
|
||||
);
|
||||
vi.mocked(applicationService.findApplicationById).mockResolvedValue(
|
||||
createMockApplication() as any,
|
||||
);
|
||||
vi.mocked(adminService.getDokployUrl).mockResolvedValue(
|
||||
"http://localhost:3000",
|
||||
);
|
||||
vi.mocked(deploymentService.createDeployment).mockResolvedValue(
|
||||
createMockDeployment() as any,
|
||||
);
|
||||
vi.mocked(execProcess.execAsync).mockResolvedValue({
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
} as any);
|
||||
vi.mocked(builders.mechanizeDockerContainer).mockResolvedValue(
|
||||
undefined as any,
|
||||
);
|
||||
vi.mocked(deploymentService.updateDeploymentStatus).mockResolvedValue(
|
||||
undefined as any,
|
||||
);
|
||||
vi.mocked(applicationService.updateApplicationStatus).mockResolvedValue(
|
||||
{} as any,
|
||||
);
|
||||
vi.mocked(notifications.sendBuildSuccessNotifications).mockResolvedValue(
|
||||
undefined as any,
|
||||
);
|
||||
vi.mocked(gitProvider.getGitCommitInfo).mockResolvedValue({
|
||||
message: "test commit",
|
||||
hash: "abc123",
|
||||
});
|
||||
vi.mocked(deploymentService.updateDeployment).mockResolvedValue({} as any);
|
||||
});
|
||||
|
||||
it("should generate correct git clone command for astro example", async () => {
|
||||
const app = createMockApplication();
|
||||
const command = await cloneGitRepository(app);
|
||||
console.log(command);
|
||||
|
||||
expect(command).toContain("https://github.com/Dokploy/examples.git");
|
||||
expect(command).not.toContain("--recurse-submodules");
|
||||
expect(command).toContain("--branch main");
|
||||
expect(command).toContain("--depth 1");
|
||||
expect(command).toContain("git clone");
|
||||
});
|
||||
|
||||
it("should generate git clone with submodules when enabled", async () => {
|
||||
const app = createMockApplication({ enableSubmodules: true });
|
||||
const command = await cloneGitRepository(app);
|
||||
|
||||
expect(command).toContain("--recurse-submodules");
|
||||
expect(command).toContain("https://github.com/Dokploy/examples.git");
|
||||
});
|
||||
|
||||
it("should verify nixpacks command is called with correct app", async () => {
|
||||
const mockNixpacksCommand = "nixpacks build /path/to/app --name test-app";
|
||||
vi.mocked(builders.getBuildCommand).mockReturnValue(mockNixpacksCommand);
|
||||
|
||||
await deployApplication({
|
||||
applicationId: "test-app-id",
|
||||
titleLog: "Test deployment",
|
||||
descriptionLog: "",
|
||||
});
|
||||
|
||||
expect(builders.getBuildCommand).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
buildType: "nixpacks",
|
||||
customGitUrl: "https://github.com/Dokploy/examples.git",
|
||||
buildPath: "/astro",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(execProcess.execAsync).toHaveBeenCalledWith(
|
||||
expect.stringContaining("nixpacks build"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should verify railpack command includes correct parameters", async () => {
|
||||
const mockApp = createMockApplication({ buildType: "railpack" });
|
||||
vi.mocked(db.query.applications.findFirst).mockResolvedValue(
|
||||
mockApp as any,
|
||||
);
|
||||
vi.mocked(applicationService.findApplicationById).mockResolvedValue(
|
||||
mockApp as any,
|
||||
);
|
||||
|
||||
const mockRailpackCommand = "railpack prepare /path/to/app";
|
||||
vi.mocked(builders.getBuildCommand).mockReturnValue(mockRailpackCommand);
|
||||
|
||||
await deployApplication({
|
||||
applicationId: "test-app-id",
|
||||
titleLog: "Railpack test",
|
||||
descriptionLog: "",
|
||||
});
|
||||
|
||||
expect(builders.getBuildCommand).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
buildType: "railpack",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(execProcess.execAsync).toHaveBeenCalledWith(
|
||||
expect.stringContaining("railpack prepare"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should execute commands in correct order", async () => {
|
||||
const mockNixpacksCommand = "nixpacks build";
|
||||
vi.mocked(builders.getBuildCommand).mockReturnValue(mockNixpacksCommand);
|
||||
|
||||
await deployApplication({
|
||||
applicationId: "test-app-id",
|
||||
titleLog: "Test",
|
||||
descriptionLog: "",
|
||||
});
|
||||
|
||||
const execCalls = vi.mocked(execProcess.execAsync).mock.calls;
|
||||
expect(execCalls.length).toBeGreaterThan(0);
|
||||
|
||||
const fullCommand = execCalls[0]?.[0];
|
||||
expect(fullCommand).toContain("set -e");
|
||||
expect(fullCommand).toContain("git clone");
|
||||
expect(fullCommand).toContain("nixpacks build");
|
||||
});
|
||||
|
||||
it("should include log redirection in command", async () => {
|
||||
const mockCommand = "nixpacks build";
|
||||
vi.mocked(builders.getBuildCommand).mockReturnValue(mockCommand);
|
||||
|
||||
await deployApplication({
|
||||
applicationId: "test-app-id",
|
||||
titleLog: "Test",
|
||||
descriptionLog: "",
|
||||
});
|
||||
|
||||
const execCalls = vi.mocked(execProcess.execAsync).mock.calls;
|
||||
const fullCommand = execCalls[0]?.[0];
|
||||
|
||||
expect(fullCommand).toContain(">> /tmp/test-deployment.log 2>&1");
|
||||
});
|
||||
});
|
||||
479
apps/dokploy/__test__/deploy/application.real.test.ts
Normal file
479
apps/dokploy/__test__/deploy/application.real.test.ts
Normal file
@@ -0,0 +1,479 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { ApplicationNested } from "@dokploy/server";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import { execAsync } from "@dokploy/server/utils/process/execAsync";
|
||||
import { format } from "date-fns";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const REAL_TEST_TIMEOUT = 180000; // 3 minutes
|
||||
|
||||
// Mock ONLY database and notifications
|
||||
vi.mock("@dokploy/server/db", () => {
|
||||
const createChainableMock = (): any => {
|
||||
const chain: any = {
|
||||
set: vi.fn(() => chain),
|
||||
where: vi.fn(() => chain),
|
||||
returning: vi.fn().mockResolvedValue([{}]),
|
||||
};
|
||||
return chain;
|
||||
};
|
||||
|
||||
return {
|
||||
db: {
|
||||
select: vi.fn(),
|
||||
insert: vi.fn(),
|
||||
update: vi.fn(() => createChainableMock()),
|
||||
delete: vi.fn(),
|
||||
query: {
|
||||
applications: {
|
||||
findFirst: vi.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@dokploy/server/services/application", async () => {
|
||||
const actual = await vi.importActual<
|
||||
typeof import("@dokploy/server/services/application")
|
||||
>("@dokploy/server/services/application");
|
||||
return {
|
||||
...actual,
|
||||
findApplicationById: vi.fn(),
|
||||
updateApplicationStatus: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@dokploy/server/services/admin", () => ({
|
||||
getDokployUrl: vi.fn().mockResolvedValue("http://localhost:3000"),
|
||||
}));
|
||||
|
||||
vi.mock("@dokploy/server/services/deployment", () => ({
|
||||
createDeployment: vi.fn(),
|
||||
updateDeploymentStatus: vi.fn(),
|
||||
updateDeployment: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@dokploy/server/utils/notifications/build-success", () => ({
|
||||
sendBuildSuccessNotifications: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@dokploy/server/utils/notifications/build-error", () => ({
|
||||
sendBuildErrorNotifications: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@dokploy/server/services/rollbacks", () => ({
|
||||
createRollback: vi.fn(),
|
||||
}));
|
||||
|
||||
// NOT mocked (executed for real):
|
||||
// - execAsync
|
||||
// - cloneGitRepository
|
||||
// - getBuildCommand
|
||||
// - mechanizeDockerContainer (requires Docker Swarm)
|
||||
|
||||
import { db } from "@dokploy/server/db";
|
||||
import * as adminService from "@dokploy/server/services/admin";
|
||||
import * as applicationService from "@dokploy/server/services/application";
|
||||
import { deployApplication } from "@dokploy/server/services/application";
|
||||
import * as deploymentService from "@dokploy/server/services/deployment";
|
||||
|
||||
const createMockApplication = (
|
||||
overrides: Partial<ApplicationNested> = {},
|
||||
): ApplicationNested =>
|
||||
({
|
||||
applicationId: "test-app-id",
|
||||
name: "Real Test App",
|
||||
appName: `real-test-${Date.now()}`,
|
||||
sourceType: "git" as const,
|
||||
customGitUrl: "https://github.com/Dokploy/examples.git",
|
||||
customGitBranch: "main",
|
||||
customGitSSHKeyId: null,
|
||||
customGitBuildPath: "/astro",
|
||||
buildType: "nixpacks" as const,
|
||||
env: "NODE_ENV=production",
|
||||
serverId: null,
|
||||
rollbackActive: false,
|
||||
enableSubmodules: false,
|
||||
environmentId: "env-id",
|
||||
environment: {
|
||||
projectId: "project-id",
|
||||
env: "",
|
||||
name: "production",
|
||||
project: {
|
||||
name: "Test Project",
|
||||
organizationId: "org-id",
|
||||
env: "",
|
||||
},
|
||||
},
|
||||
domains: [],
|
||||
mounts: [],
|
||||
security: [],
|
||||
redirects: [],
|
||||
ports: [],
|
||||
registry: null,
|
||||
...overrides,
|
||||
}) as ApplicationNested;
|
||||
|
||||
const createMockDeployment = async (appName: string) => {
|
||||
const { LOGS_PATH } = paths(false); // false = local, no remote server
|
||||
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
||||
const fileName = `${appName}-${formattedDateTime}.log`;
|
||||
const logFilePath = path.join(LOGS_PATH, appName, fileName);
|
||||
|
||||
// Actually create the log directory
|
||||
await execAsync(`mkdir -p ${path.dirname(logFilePath)}`);
|
||||
await execAsync(`echo "Initializing deployment" > ${logFilePath}`);
|
||||
|
||||
return {
|
||||
deploymentId: "deployment-id",
|
||||
logPath: logFilePath,
|
||||
};
|
||||
};
|
||||
|
||||
async function cleanupDocker(appName: string) {
|
||||
try {
|
||||
await execAsync(`docker stop ${appName} 2>/dev/null || true`);
|
||||
await execAsync(`docker rm ${appName} 2>/dev/null || true`);
|
||||
await execAsync(`docker rmi ${appName} 2>/dev/null || true`);
|
||||
} catch (error) {
|
||||
console.log("Docker cleanup completed");
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanupFiles(appName: string) {
|
||||
try {
|
||||
const { LOGS_PATH, APPLICATIONS_PATH } = paths(false);
|
||||
|
||||
// Clean cloned code directories
|
||||
const appPath = path.join(APPLICATIONS_PATH, appName);
|
||||
await execAsync(`rm -rf ${appPath} 2>/dev/null || true`);
|
||||
|
||||
// Clean logs for appName - removes entire folder
|
||||
const logPath = path.join(LOGS_PATH, appName);
|
||||
await execAsync(`rm -rf ${logPath} 2>/dev/null || true`);
|
||||
|
||||
console.log(`✅ Cleaned up files and logs for ${appName}`);
|
||||
} catch (error) {
|
||||
console.error(`⚠️ Error during cleanup for ${appName}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
describe(
|
||||
"deployApplication - REAL Execution Tests",
|
||||
() => {
|
||||
let currentAppName: string;
|
||||
let currentDeployment: any;
|
||||
const allTestAppNames: string[] = [];
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
currentAppName = `real-test-${Date.now()}`;
|
||||
currentDeployment = await createMockDeployment(currentAppName);
|
||||
allTestAppNames.push(currentAppName);
|
||||
|
||||
const mockApp = createMockApplication({ appName: currentAppName });
|
||||
|
||||
vi.mocked(db.query.applications.findFirst).mockResolvedValue(
|
||||
mockApp as any,
|
||||
);
|
||||
vi.mocked(applicationService.findApplicationById).mockResolvedValue(
|
||||
mockApp as any,
|
||||
);
|
||||
vi.mocked(adminService.getDokployUrl).mockResolvedValue(
|
||||
"http://localhost:3000",
|
||||
);
|
||||
vi.mocked(deploymentService.createDeployment).mockResolvedValue(
|
||||
currentDeployment as any,
|
||||
);
|
||||
vi.mocked(deploymentService.updateDeploymentStatus).mockResolvedValue(
|
||||
undefined as any,
|
||||
);
|
||||
vi.mocked(applicationService.updateApplicationStatus).mockResolvedValue(
|
||||
{} as any,
|
||||
);
|
||||
vi.mocked(deploymentService.updateDeployment).mockResolvedValue(
|
||||
{} as any,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// ALWAYS cleanup, even if test failed or passed
|
||||
console.log(`\n🧹 Cleaning up test: ${currentAppName}`);
|
||||
|
||||
// Clean current appName
|
||||
try {
|
||||
await cleanupDocker(currentAppName);
|
||||
await cleanupFiles(currentAppName);
|
||||
} catch (error) {
|
||||
console.error("⚠️ Error cleaning current app:", error);
|
||||
}
|
||||
|
||||
// Clean ALL test folders just in case
|
||||
try {
|
||||
const { LOGS_PATH, APPLICATIONS_PATH } = paths(false);
|
||||
await execAsync(`rm -rf ${LOGS_PATH}/real-* 2>/dev/null || true`);
|
||||
await execAsync(
|
||||
`rm -rf ${APPLICATIONS_PATH}/real-* 2>/dev/null || true`,
|
||||
);
|
||||
console.log("✅ Cleaned up all test artifacts");
|
||||
} catch (error) {
|
||||
console.error("⚠️ Error cleaning all artifacts:", error);
|
||||
}
|
||||
|
||||
console.log("✅ Cleanup completed\n");
|
||||
});
|
||||
|
||||
it(
|
||||
"should REALLY clone git repo and build with nixpacks",
|
||||
async () => {
|
||||
console.log(`\n🚀 Testing real deployment with app: ${currentAppName}`);
|
||||
|
||||
const result = await deployApplication({
|
||||
applicationId: "test-app-id",
|
||||
titleLog: "Real Nixpacks Test",
|
||||
descriptionLog: "Testing real execution",
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
|
||||
// Verify that Docker image was actually created
|
||||
const { stdout: dockerImages } = await execAsync(
|
||||
`docker images ${currentAppName} --format "{{.Repository}}"`,
|
||||
);
|
||||
console.log("dockerImages", dockerImages);
|
||||
expect(dockerImages.trim()).toBe(currentAppName);
|
||||
console.log(`✅ Docker image created: ${currentAppName}`);
|
||||
|
||||
// Verify log exists and has content
|
||||
expect(existsSync(currentDeployment.logPath)).toBe(true);
|
||||
const { stdout: logContent } = await execAsync(
|
||||
`cat ${currentDeployment.logPath}`,
|
||||
);
|
||||
expect(logContent).toContain("Cloning");
|
||||
expect(logContent).toContain("nixpacks");
|
||||
console.log(`✅ Build log created with ${logContent.length} chars`);
|
||||
|
||||
// Verify update functions were called
|
||||
expect(deploymentService.updateDeploymentStatus).toHaveBeenCalledWith(
|
||||
"deployment-id",
|
||||
"done",
|
||||
);
|
||||
},
|
||||
REAL_TEST_TIMEOUT,
|
||||
);
|
||||
|
||||
it.skip(
|
||||
"should REALLY build with railpack (SKIPPED: requires special permissions)",
|
||||
async () => {
|
||||
const railpackAppName = `real-railpack-${Date.now()}`;
|
||||
const railpackApp = createMockApplication({
|
||||
appName: railpackAppName,
|
||||
buildType: "railpack",
|
||||
railpackVersion: "3",
|
||||
});
|
||||
currentAppName = railpackAppName;
|
||||
allTestAppNames.push(railpackAppName);
|
||||
|
||||
vi.mocked(db.query.applications.findFirst).mockResolvedValue(
|
||||
railpackApp as any,
|
||||
);
|
||||
vi.mocked(applicationService.findApplicationById).mockResolvedValue(
|
||||
railpackApp as any,
|
||||
);
|
||||
|
||||
console.log(`\n🚀 Testing real railpack deployment: ${currentAppName}`);
|
||||
|
||||
const result = await deployApplication({
|
||||
applicationId: "test-app-id",
|
||||
titleLog: "Real Railpack Test",
|
||||
descriptionLog: "",
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
|
||||
const { stdout: dockerImages } = await execAsync(
|
||||
`docker images ${currentAppName} --format "{{.Repository}}"`,
|
||||
);
|
||||
expect(dockerImages.trim()).toBe(currentAppName);
|
||||
console.log(`✅ Railpack image created: ${currentAppName}`);
|
||||
|
||||
const { stdout: logContent } = await execAsync(
|
||||
`cat ${currentDeployment.logPath}`,
|
||||
);
|
||||
expect(logContent).toContain("railpack");
|
||||
console.log("✅ Railpack build completed");
|
||||
},
|
||||
REAL_TEST_TIMEOUT,
|
||||
);
|
||||
|
||||
it(
|
||||
"should handle REAL git clone errors",
|
||||
async () => {
|
||||
const errorAppName = `real-error-${Date.now()}`;
|
||||
const errorApp = createMockApplication({
|
||||
appName: errorAppName,
|
||||
customGitUrl:
|
||||
"https://github.com/invalid/nonexistent-repo-123456.git",
|
||||
});
|
||||
currentAppName = errorAppName;
|
||||
allTestAppNames.push(errorAppName);
|
||||
|
||||
vi.mocked(db.query.applications.findFirst).mockResolvedValue(
|
||||
errorApp as any,
|
||||
);
|
||||
vi.mocked(applicationService.findApplicationById).mockResolvedValue(
|
||||
errorApp as any,
|
||||
);
|
||||
|
||||
console.log(`\n🚀 Testing real error handling: ${currentAppName}`);
|
||||
|
||||
await expect(
|
||||
deployApplication({
|
||||
applicationId: "test-app-id",
|
||||
titleLog: "Real Error Test",
|
||||
descriptionLog: "",
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
|
||||
// Verify error status was called
|
||||
expect(deploymentService.updateDeploymentStatus).toHaveBeenCalledWith(
|
||||
"deployment-id",
|
||||
"error",
|
||||
);
|
||||
|
||||
// Verify log contains error
|
||||
const { stdout: logContent } = await execAsync(
|
||||
`cat ${currentDeployment.logPath}`,
|
||||
);
|
||||
expect(logContent.toLowerCase()).toContain("error");
|
||||
console.log("✅ Error handling verified");
|
||||
},
|
||||
REAL_TEST_TIMEOUT,
|
||||
);
|
||||
|
||||
it(
|
||||
"should REALLY clone with submodules when enabled",
|
||||
async () => {
|
||||
const submodulesAppName = `real-submodules-${Date.now()}`;
|
||||
const submodulesApp = createMockApplication({
|
||||
appName: submodulesAppName,
|
||||
enableSubmodules: true,
|
||||
});
|
||||
currentAppName = submodulesAppName;
|
||||
allTestAppNames.push(submodulesAppName);
|
||||
|
||||
vi.mocked(db.query.applications.findFirst).mockResolvedValue(
|
||||
submodulesApp as any,
|
||||
);
|
||||
vi.mocked(applicationService.findApplicationById).mockResolvedValue(
|
||||
submodulesApp as any,
|
||||
);
|
||||
|
||||
console.log(`\n🚀 Testing real submodules support: ${currentAppName}`);
|
||||
|
||||
const result = await deployApplication({
|
||||
applicationId: "test-app-id",
|
||||
titleLog: "Real Submodules Test",
|
||||
descriptionLog: "",
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
|
||||
// Verify deployment completed successfully
|
||||
const { stdout: logContent } = await execAsync(
|
||||
`cat ${currentDeployment.logPath}`,
|
||||
);
|
||||
expect(logContent).toContain("Cloning");
|
||||
expect(logContent.length).toBeGreaterThan(100);
|
||||
console.log("✅ Submodules deployment completed");
|
||||
|
||||
// Verify image
|
||||
const { stdout: dockerImages } = await execAsync(
|
||||
`docker images ${currentAppName} --format "{{.Repository}}"`,
|
||||
);
|
||||
expect(dockerImages.trim()).toBe(currentAppName);
|
||||
},
|
||||
REAL_TEST_TIMEOUT,
|
||||
);
|
||||
|
||||
it(
|
||||
"should verify REAL commit info extraction",
|
||||
async () => {
|
||||
console.log(`\n🚀 Testing real commit info: ${currentAppName}`);
|
||||
|
||||
await deployApplication({
|
||||
applicationId: "test-app-id",
|
||||
titleLog: "Real Commit Test",
|
||||
descriptionLog: "",
|
||||
});
|
||||
|
||||
// Verify updateDeployment was called with commit info
|
||||
expect(deploymentService.updateDeployment).toHaveBeenCalled();
|
||||
const updateCall = vi.mocked(deploymentService.updateDeployment).mock
|
||||
.calls[0];
|
||||
|
||||
// Real commit info should have title and hash
|
||||
expect(updateCall?.[1]).toHaveProperty("title");
|
||||
expect(updateCall?.[1]).toHaveProperty("description");
|
||||
expect(updateCall?.[1]?.description).toContain("Commit:");
|
||||
|
||||
console.log(
|
||||
`✅ Real commit extracted: ${updateCall?.[1]?.title?.substring(0, 50)}...`,
|
||||
);
|
||||
},
|
||||
REAL_TEST_TIMEOUT,
|
||||
);
|
||||
|
||||
it(
|
||||
"should REALLY build with Dockerfile",
|
||||
async () => {
|
||||
const dockerfileAppName = `real-dockerfile-${Date.now()}`;
|
||||
const dockerfileApp = createMockApplication({
|
||||
appName: dockerfileAppName,
|
||||
buildType: "dockerfile",
|
||||
customGitBuildPath: "/deno",
|
||||
dockerfile: "Dockerfile",
|
||||
});
|
||||
currentAppName = dockerfileAppName;
|
||||
allTestAppNames.push(dockerfileAppName);
|
||||
|
||||
vi.mocked(db.query.applications.findFirst).mockResolvedValue(
|
||||
dockerfileApp as any,
|
||||
);
|
||||
vi.mocked(applicationService.findApplicationById).mockResolvedValue(
|
||||
dockerfileApp as any,
|
||||
);
|
||||
|
||||
console.log(`\n🚀 Testing real Dockerfile build: ${currentAppName}`);
|
||||
|
||||
const result = await deployApplication({
|
||||
applicationId: "test-app-id",
|
||||
titleLog: "Real Dockerfile Test",
|
||||
descriptionLog: "",
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
|
||||
// Verify log
|
||||
const { stdout: logContent } = await execAsync(
|
||||
`cat ${currentDeployment.logPath}`,
|
||||
);
|
||||
expect(logContent).toContain("Building");
|
||||
expect(logContent).toContain(dockerfileAppName);
|
||||
console.log("✅ Dockerfile build log verified");
|
||||
|
||||
// Verify image
|
||||
const { stdout: dockerImages } = await execAsync(
|
||||
`docker images ${currentAppName} --format "{{.Repository}}"`,
|
||||
);
|
||||
console.log("dockerImages", dockerImages);
|
||||
expect(dockerImages.trim()).toBe(currentAppName);
|
||||
console.log(`✅ Docker image created: ${currentAppName}`);
|
||||
},
|
||||
REAL_TEST_TIMEOUT,
|
||||
);
|
||||
},
|
||||
REAL_TEST_TIMEOUT,
|
||||
);
|
||||
@@ -30,6 +30,10 @@ const baseApp: ApplicationNested = {
|
||||
previewLabels: [],
|
||||
herokuVersion: "",
|
||||
giteaBranch: "",
|
||||
buildServerId: "",
|
||||
buildRegistryId: "",
|
||||
buildRegistry: null,
|
||||
args: [],
|
||||
giteaBuildPath: "",
|
||||
previewRequireCollaboratorPermissions: false,
|
||||
giteaId: "",
|
||||
|
||||
311
apps/dokploy/__test__/env/environment.test.ts
vendored
311
apps/dokploy/__test__/env/environment.test.ts
vendored
@@ -1,4 +1,7 @@
|
||||
import { prepareEnvironmentVariables } from "@dokploy/server/index";
|
||||
import {
|
||||
prepareEnvironmentVariables,
|
||||
prepareEnvironmentVariablesForShell,
|
||||
} from "@dokploy/server/index";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
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é");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,11 @@ import type { ApplicationNested } from "@dokploy/server/utils/builders";
|
||||
import { mechanizeDockerContainer } from "@dokploy/server/utils/builders";
|
||||
|
||||
type MockCreateServiceOptions = {
|
||||
StopGracePeriod?: number;
|
||||
TaskTemplate?: {
|
||||
ContainerSpec?: {
|
||||
StopGracePeriod?: number;
|
||||
};
|
||||
};
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
@@ -82,8 +86,10 @@ describe("mechanizeDockerContainer", () => {
|
||||
throw new Error("createServiceMock should have been called once");
|
||||
}
|
||||
const [settings] = call;
|
||||
expect(settings.StopGracePeriod).toBe(0);
|
||||
expect(typeof settings.StopGracePeriod).toBe("number");
|
||||
expect(settings.TaskTemplate?.ContainerSpec?.StopGracePeriod).toBe(0);
|
||||
expect(typeof settings.TaskTemplate?.ContainerSpec?.StopGracePeriod).toBe(
|
||||
"number",
|
||||
);
|
||||
});
|
||||
|
||||
it("omits StopGracePeriod when stopGracePeriodSwarm is null", async () => {
|
||||
@@ -97,6 +103,8 @@ describe("mechanizeDockerContainer", () => {
|
||||
throw new Error("createServiceMock should have been called once");
|
||||
}
|
||||
const [settings] = call;
|
||||
expect(settings).not.toHaveProperty("StopGracePeriod");
|
||||
expect(settings.TaskTemplate?.ContainerSpec).not.toHaveProperty(
|
||||
"StopGracePeriod",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,8 +11,12 @@ const baseApp: ApplicationNested = {
|
||||
giteaRepository: "",
|
||||
giteaOwner: "",
|
||||
giteaBranch: "",
|
||||
buildServerId: "",
|
||||
buildRegistryId: "",
|
||||
buildRegistry: null,
|
||||
giteaBuildPath: "",
|
||||
giteaId: "",
|
||||
args: [],
|
||||
cleanCache: false,
|
||||
applicationStatus: "done",
|
||||
endpointSpecSwarm: null,
|
||||
|
||||
@@ -13,7 +13,11 @@ export default defineConfig({
|
||||
NODE: "test",
|
||||
},
|
||||
},
|
||||
plugins: [tsconfigPaths()],
|
||||
plugins: [
|
||||
tsconfigPaths({
|
||||
projects: [path.resolve(__dirname, "../tsconfig.json")],
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@dokploy/server": path.resolve(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Plus, Trash2 } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useFieldArray, useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -28,6 +29,13 @@ interface Props {
|
||||
|
||||
const AddRedirectSchema = z.object({
|
||||
command: z.string(),
|
||||
args: z
|
||||
.array(
|
||||
z.object({
|
||||
value: z.string().min(1, "Argument cannot be empty"),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
type AddCommand = z.infer<typeof AddRedirectSchema>;
|
||||
@@ -47,22 +55,30 @@ export const AddCommand = ({ applicationId }: Props) => {
|
||||
const form = useForm<AddCommand>({
|
||||
defaultValues: {
|
||||
command: "",
|
||||
args: [],
|
||||
},
|
||||
resolver: zodResolver(AddRedirectSchema),
|
||||
});
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control: form.control,
|
||||
name: "args",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.command) {
|
||||
if (data) {
|
||||
form.reset({
|
||||
command: data?.command || "",
|
||||
args: data?.args?.map((arg) => ({ value: arg })) || [],
|
||||
});
|
||||
}
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful, data?.command]);
|
||||
}, [data, form]);
|
||||
|
||||
const onSubmit = async (data: AddCommand) => {
|
||||
await mutateAsync({
|
||||
applicationId,
|
||||
command: data?.command,
|
||||
args: data?.args?.map((arg) => arg.value).filter(Boolean),
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Command Updated");
|
||||
@@ -100,13 +116,65 @@ export const AddCommand = ({ applicationId }: Props) => {
|
||||
<FormItem>
|
||||
<FormLabel>Command</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Custom command" {...field} />
|
||||
<Input placeholder="/bin/sh" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<FormLabel>Arguments (Args)</FormLabel>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => append({ value: "" })}
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
Add Argument
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{fields.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No arguments added yet. Click "Add Argument" to add one.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{fields.map((field, index) => (
|
||||
<FormField
|
||||
key={field.id}
|
||||
control={form.control}
|
||||
name={`args.${index}.value`}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="flex gap-2">
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={
|
||||
index === 0 ? "-c" : "echo Hello World"
|
||||
}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Button isLoading={isLoading} type="submit" className="w-fit">
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Server } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
const schema = z.object({
|
||||
buildServerId: z.string().min(1, "Build server is required"),
|
||||
buildRegistryId: z.string().min(1, "Build registry is required"),
|
||||
});
|
||||
|
||||
type Schema = z.infer<typeof schema>;
|
||||
|
||||
export const ShowBuildServer = ({ applicationId }: Props) => {
|
||||
const { data, refetch } = api.application.one.useQuery(
|
||||
{ applicationId },
|
||||
{ enabled: !!applicationId },
|
||||
);
|
||||
const { data: buildServers } = api.server.buildServers.useQuery();
|
||||
const { data: registries } = api.registry.all.useQuery();
|
||||
|
||||
const { mutateAsync, isLoading } = api.application.update.useMutation();
|
||||
|
||||
const form = useForm<Schema>({
|
||||
defaultValues: {
|
||||
buildServerId: data?.buildServerId || "",
|
||||
buildRegistryId: data?.buildRegistryId || "",
|
||||
},
|
||||
resolver: zodResolver(schema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
buildServerId: data?.buildServerId || "",
|
||||
buildRegistryId: data?.buildRegistryId || "",
|
||||
});
|
||||
}
|
||||
}, [form, form.reset, data]);
|
||||
|
||||
const onSubmit = async (formData: Schema) => {
|
||||
await mutateAsync({
|
||||
applicationId,
|
||||
buildServerId:
|
||||
formData?.buildServerId === "none" || !formData?.buildServerId
|
||||
? null
|
||||
: formData?.buildServerId,
|
||||
buildRegistryId:
|
||||
formData?.buildRegistryId === "none" || !formData?.buildRegistryId
|
||||
? null
|
||||
: formData?.buildRegistryId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Build Server Settings Updated");
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error updating build server settings");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="bg-background">
|
||||
<CardHeader>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Server className="size-6 text-muted-foreground" />
|
||||
<div>
|
||||
<CardTitle className="text-xl">Build Server</CardTitle>
|
||||
<CardDescription>
|
||||
Configure a dedicated server for building your application.
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<AlertBlock type="info">
|
||||
Build servers offload the build process from your deployment servers.
|
||||
Select a build server and registry to use for building your
|
||||
application.
|
||||
</AlertBlock>
|
||||
|
||||
<AlertBlock type="info">
|
||||
📊 <strong>Important:</strong> Once the build finishes, you'll need to
|
||||
wait a few seconds for the deployment server to download the image.
|
||||
These download logs will <strong>NOT</strong> appear in the build
|
||||
deployment logs. Check the <strong>Logs</strong> tab to see when the
|
||||
container starts running.
|
||||
</AlertBlock>
|
||||
|
||||
{!registries || registries.length === 0 ? (
|
||||
<AlertBlock type="warning">
|
||||
You need to add at least one registry to use build servers. Please
|
||||
go to{" "}
|
||||
<Link
|
||||
href="/dashboard/settings/registry"
|
||||
className="text-primary underline"
|
||||
>
|
||||
Settings
|
||||
</Link>{" "}
|
||||
to add a registry.
|
||||
</AlertBlock>
|
||||
) : null}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="buildServerId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Build Server</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={field.value || "none"}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a build server" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="none">
|
||||
<span className="flex items-center gap-2">
|
||||
<span>None</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
{buildServers?.map((server) => (
|
||||
<SelectItem
|
||||
key={server.serverId}
|
||||
value={server.serverId}
|
||||
>
|
||||
<span className="flex items-center gap-2 justify-between w-full">
|
||||
<span>{server.name}</span>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{server.ipAddress}
|
||||
</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectLabel>
|
||||
Build Servers ({buildServers?.length || 0})
|
||||
</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
Select a build server to handle the build process for this
|
||||
application.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="buildRegistryId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Build Registry</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={field.value || "none"}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a registry" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="none">
|
||||
<span className="flex items-center gap-2">
|
||||
<span>None</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
{registries?.map((registry) => (
|
||||
<SelectItem
|
||||
key={registry.registryId}
|
||||
value={registry.registryId}
|
||||
>
|
||||
{registry.registryName}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectLabel>
|
||||
Registries ({registries?.length || 0})
|
||||
</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
Select a registry to store the built images from the build
|
||||
server.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex w-full justify-end">
|
||||
<Button isLoading={isLoading} type="submit">
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -407,7 +407,7 @@ export const ShowDeployments = ({
|
||||
</div>
|
||||
)}
|
||||
<ShowDeployment
|
||||
serverId={serverId}
|
||||
serverId={activeLog?.buildServerId || serverId}
|
||||
open={Boolean(activeLog && activeLog.logPath !== null)}
|
||||
onClose={() => setActiveLog(null)}
|
||||
logPath={activeLog?.logPath || ""}
|
||||
|
||||
@@ -46,7 +46,13 @@ export type CacheType = "fetch" | "cache";
|
||||
|
||||
export const domain = z
|
||||
.object({
|
||||
host: z.string().min(1, { message: "Add a hostname" }),
|
||||
host: z
|
||||
.string()
|
||||
.min(1, { message: "Add a hostname" })
|
||||
.refine((val) => val === val.trim(), {
|
||||
message: "Domain name cannot have leading or trailing spaces",
|
||||
})
|
||||
.transform((val) => val.trim()),
|
||||
path: z.string().min(1).optional(),
|
||||
internalPath: z.string().optional(),
|
||||
stripPath: z.boolean().optional(),
|
||||
@@ -299,6 +305,13 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
{type === "compose" && (
|
||||
<AlertBlock type="info" className="mb-4">
|
||||
Whenever you make changes to domains, remember to redeploy your
|
||||
compose to apply the changes.
|
||||
</AlertBlock>
|
||||
)}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Plus, Trash2 } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useFieldArray, useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -20,6 +21,13 @@ import type { ServiceType } from "../../application/advanced/show-resources";
|
||||
const addDockerImage = z.object({
|
||||
dockerImage: z.string().min(1, "Docker image is required"),
|
||||
command: z.string(),
|
||||
args: z
|
||||
.array(
|
||||
z.object({
|
||||
value: z.string().min(1, "Argument cannot be empty"),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
interface Props {
|
||||
@@ -61,18 +69,25 @@ export const ShowCustomCommand = ({ id, type }: Props) => {
|
||||
defaultValues: {
|
||||
dockerImage: "",
|
||||
command: "",
|
||||
args: [],
|
||||
},
|
||||
resolver: zodResolver(addDockerImage),
|
||||
});
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control: form.control,
|
||||
name: "args",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
dockerImage: data.dockerImage,
|
||||
command: data.command || "",
|
||||
args: data.args?.map((arg) => ({ value: arg })) || [],
|
||||
});
|
||||
}
|
||||
}, [data, form, form.reset]);
|
||||
}, [data, form]);
|
||||
|
||||
const onSubmit = async (formData: AddDockerImage) => {
|
||||
await mutateAsync({
|
||||
@@ -83,6 +98,7 @@ export const ShowCustomCommand = ({ id, type }: Props) => {
|
||||
mariadbId: id || "",
|
||||
dockerImage: formData?.dockerImage,
|
||||
command: formData?.command,
|
||||
args: formData?.args?.map((arg) => arg.value).filter(Boolean),
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Custom Command Updated");
|
||||
@@ -128,13 +144,68 @@ export const ShowCustomCommand = ({ id, type }: Props) => {
|
||||
<FormItem>
|
||||
<FormLabel>Command</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Custom command" {...field} />
|
||||
<Input placeholder="/bin/sh" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<FormLabel>Arguments (Args)</FormLabel>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => append({ value: "" })}
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
Add Argument
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{fields.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No arguments added yet. Click "Add Argument" to add one.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{fields.map((field, index) => (
|
||||
<FormField
|
||||
key={field.id}
|
||||
control={form.control}
|
||||
name={`args.${index}.value`}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="flex gap-2">
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={
|
||||
index === 0
|
||||
? "-c"
|
||||
: "redis-server --port 6379"
|
||||
}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex w-full justify-end">
|
||||
<Button isLoading={form.formState.isSubmitting} type="submit">
|
||||
Save
|
||||
|
||||
@@ -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);
|
||||
}}
|
||||
|
||||
@@ -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);
|
||||
}}
|
||||
|
||||
@@ -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);
|
||||
}}
|
||||
|
||||
@@ -138,7 +138,7 @@ export const ShowProjects = () => {
|
||||
list={[{ name: "Projects", href: "/dashboard/projects" }]}
|
||||
/>
|
||||
{!isCloud && (
|
||||
<div className="absolute top-5 right-5">
|
||||
<div className="absolute top-4 right-4">
|
||||
<TimeBadge />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -49,51 +49,65 @@ export const RequestDistributionChart = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height={200}>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<AreaChart
|
||||
accessibilityLayer
|
||||
data={stats || []}
|
||||
margin={{
|
||||
left: 12,
|
||||
right: 12,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="hour"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickFormatter={(value) =>
|
||||
new Date(value).toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})
|
||||
}
|
||||
/>
|
||||
<YAxis tickLine={false} axisLine={false} tickMargin={8} />
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent indicator="line" />}
|
||||
labelFormatter={(value) =>
|
||||
new Date(value).toLocaleString([], {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Area
|
||||
dataKey="count"
|
||||
type="natural"
|
||||
fill="hsl(var(--chart-1))"
|
||||
fillOpacity={0.4}
|
||||
stroke="hsl(var(--chart-1))"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</ResponsiveContainer>
|
||||
<div className="w-full h-[200px] overflow-hidden">
|
||||
<ResponsiveContainer
|
||||
width="100%"
|
||||
height="100%"
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<AreaChart
|
||||
accessibilityLayer
|
||||
data={stats || []}
|
||||
margin={{
|
||||
top: 10,
|
||||
left: 12,
|
||||
right: 12,
|
||||
bottom: 0,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="hour"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickFormatter={(value) =>
|
||||
new Date(value).toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})
|
||||
}
|
||||
/>
|
||||
<YAxis
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
allowDataOverflow={false}
|
||||
domain={[0, "auto"]}
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent indicator="line" />}
|
||||
labelFormatter={(value) =>
|
||||
new Date(value).toLocaleString([], {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Area
|
||||
dataKey="count"
|
||||
type="monotone"
|
||||
fill="hsl(var(--chart-1))"
|
||||
fillOpacity={0.4}
|
||||
stroke="hsl(var(--chart-1))"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -51,13 +51,19 @@ export const ShowRequests = () => {
|
||||
const { mutateAsync: updateLogCleanup } =
|
||||
api.settings.updateLogCleanup.useMutation();
|
||||
const [cronExpression, setCronExpression] = useState<string | null>(null);
|
||||
|
||||
// Set default date range to last 3 days
|
||||
const getDefaultDateRange = () => {
|
||||
const to = new Date();
|
||||
const from = new Date();
|
||||
from.setDate(from.getDate() - 3);
|
||||
return { from, to };
|
||||
};
|
||||
|
||||
const [dateRange, setDateRange] = useState<{
|
||||
from: Date | undefined;
|
||||
to: Date | undefined;
|
||||
}>({
|
||||
from: undefined,
|
||||
to: undefined,
|
||||
});
|
||||
}>(getDefaultDateRange());
|
||||
|
||||
useEffect(() => {
|
||||
if (logCleanupStatus) {
|
||||
@@ -169,17 +175,13 @@ export const ShowRequests = () => {
|
||||
{isActive ? (
|
||||
<>
|
||||
<div className="flex justify-end mb-4 gap-2">
|
||||
{(dateRange.from || dateRange.to) && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
setDateRange({ from: undefined, to: undefined })
|
||||
}
|
||||
className="px-3"
|
||||
>
|
||||
Clear dates
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setDateRange(getDefaultDateRange())}
|
||||
className="px-3"
|
||||
>
|
||||
Reset to Last 3 Days
|
||||
</Button>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
"use client";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { PenBoxIcon, PlusIcon } from "lucide-react";
|
||||
import { Check, ChevronDown, PenBoxIcon, PlusIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -26,13 +33,12 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
const Schema = z.object({
|
||||
@@ -53,6 +59,8 @@ export const HandleAi = ({ aiId }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [modelPopoverOpen, setModelPopoverOpen] = useState(false);
|
||||
const [modelSearch, setModelSearch] = useState("");
|
||||
const { data, refetch } = api.ai.one.useQuery(
|
||||
{
|
||||
aiId: aiId || "",
|
||||
@@ -77,13 +85,17 @@ export const HandleAi = ({ aiId }: Props) => {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
form.reset({
|
||||
name: data?.name ?? "",
|
||||
apiUrl: data?.apiUrl ?? "https://api.openai.com/v1",
|
||||
apiKey: data?.apiKey ?? "",
|
||||
model: data?.model ?? "",
|
||||
isEnabled: data?.isEnabled ?? true,
|
||||
});
|
||||
if (data) {
|
||||
form.reset({
|
||||
name: data?.name ?? "",
|
||||
apiUrl: data?.apiUrl ?? "https://api.openai.com/v1",
|
||||
apiKey: data?.apiKey ?? "",
|
||||
model: data?.model ?? "",
|
||||
isEnabled: data?.isEnabled ?? true,
|
||||
});
|
||||
}
|
||||
setModelSearch("");
|
||||
setModelPopoverOpen(false);
|
||||
}, [aiId, form, data]);
|
||||
|
||||
const apiUrl = form.watch("apiUrl");
|
||||
@@ -104,14 +116,6 @@ export const HandleAi = ({ aiId }: Props) => {
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const apiUrl = form.watch("apiUrl");
|
||||
const apiKey = form.watch("apiKey");
|
||||
if (apiUrl && apiKey) {
|
||||
form.setValue("model", "");
|
||||
}
|
||||
}, [form.watch("apiUrl"), form.watch("apiKey")]);
|
||||
|
||||
const onSubmit = async (data: Schema) => {
|
||||
try {
|
||||
await mutateAsync({
|
||||
@@ -131,7 +135,16 @@ export const HandleAi = ({ aiId }: Props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(isOpen) => {
|
||||
setOpen(isOpen);
|
||||
if (!isOpen) {
|
||||
setModelSearch("");
|
||||
setModelPopoverOpen(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogTrigger className="" asChild>
|
||||
{aiId ? (
|
||||
<Button
|
||||
@@ -182,7 +195,17 @@ export const HandleAi = ({ aiId }: Props) => {
|
||||
<FormItem>
|
||||
<FormLabel>API URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="https://api.openai.com/v1" {...field} />
|
||||
<Input
|
||||
placeholder="https://api.openai.com/v1"
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
field.onChange(e);
|
||||
// Reset model when user changes API URL
|
||||
if (form.getValues("model")) {
|
||||
form.setValue("model", "");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
The base URL for your AI provider's API
|
||||
@@ -205,6 +228,13 @@ export const HandleAi = ({ aiId }: Props) => {
|
||||
placeholder="sk-..."
|
||||
autoComplete="one-time-code"
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
field.onChange(e);
|
||||
// Reset model when user changes API Key
|
||||
if (form.getValues("model")) {
|
||||
form.setValue("model", "");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
@@ -232,30 +262,89 @@ export const HandleAi = ({ aiId }: Props) => {
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="model"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Model</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={field.value || ""}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a model" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{models.map((model) => (
|
||||
<SelectItem key={model.id} value={model.id}>
|
||||
{model.id}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>Select an AI model to use</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
render={({ field }) => {
|
||||
const selectedModel = models.find(
|
||||
(m) => m.id === field.value,
|
||||
);
|
||||
const filteredModels = models.filter((model) =>
|
||||
model.id.toLowerCase().includes(modelSearch.toLowerCase()),
|
||||
);
|
||||
|
||||
// Ensure selected model is always in the filtered list
|
||||
const displayModels =
|
||||
field.value &&
|
||||
!filteredModels.find((m) => m.id === field.value) &&
|
||||
selectedModel
|
||||
? [selectedModel, ...filteredModels]
|
||||
: filteredModels;
|
||||
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Model</FormLabel>
|
||||
<Popover
|
||||
open={modelPopoverOpen}
|
||||
onOpenChange={setModelPopoverOpen}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{field.value
|
||||
? (selectedModel?.id ?? field.value)
|
||||
: "Select a model"}
|
||||
<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[400px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search models..."
|
||||
value={modelSearch}
|
||||
onValueChange={setModelSearch}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>No models found.</CommandEmpty>
|
||||
{displayModels.map((model) => {
|
||||
const isSelected = field.value === model.id;
|
||||
return (
|
||||
<CommandItem
|
||||
key={model.id}
|
||||
value={model.id}
|
||||
onSelect={() => {
|
||||
field.onChange(model.id);
|
||||
setModelPopoverOpen(false);
|
||||
setModelSearch("");
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
isSelected
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
{model.id}
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormDescription>
|
||||
Select an AI model to use
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ export const notificationSchema = z.discriminatedUnion("type", [
|
||||
type: z.literal("ntfy"),
|
||||
serverUrl: z.string().min(1, { message: "Server URL is required" }),
|
||||
topic: z.string().min(1, { message: "Topic is required" }),
|
||||
accessToken: z.string().min(1, { message: "Access Token is required" }),
|
||||
accessToken: z.string().optional(),
|
||||
priority: z.number().min(1).max(5).default(3),
|
||||
})
|
||||
.merge(notificationBaseSchema),
|
||||
@@ -303,7 +303,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
dokployRestart: notification.dokployRestart,
|
||||
databaseBackup: notification.databaseBackup,
|
||||
type: notification.notificationType,
|
||||
accessToken: notification.ntfy?.accessToken,
|
||||
accessToken: notification.ntfy?.accessToken || "",
|
||||
topic: notification.ntfy?.topic,
|
||||
priority: notification.ntfy?.priority,
|
||||
serverUrl: notification.ntfy?.serverUrl,
|
||||
@@ -432,7 +432,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
dokployRestart: dokployRestart,
|
||||
databaseBackup: databaseBackup,
|
||||
serverUrl: data.serverUrl,
|
||||
accessToken: data.accessToken,
|
||||
accessToken: data.accessToken || "",
|
||||
topic: data.topic,
|
||||
priority: data.priority,
|
||||
name: data.name,
|
||||
@@ -1001,8 +1001,12 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
<Input
|
||||
placeholder="AzxcvbnmKjhgfdsa..."
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Optional. Leave blank for public topics.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -1214,55 +1218,63 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
isLoadingLark
|
||||
}
|
||||
variant="secondary"
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
const isValid = await form.trigger();
|
||||
if (!isValid) return;
|
||||
|
||||
const data = form.getValues();
|
||||
|
||||
try {
|
||||
if (type === "slack") {
|
||||
if (data.type === "slack") {
|
||||
await testSlackConnection({
|
||||
webhookUrl: form.getValues("webhookUrl"),
|
||||
channel: form.getValues("channel"),
|
||||
webhookUrl: data.webhookUrl,
|
||||
channel: data.channel,
|
||||
});
|
||||
} else if (type === "telegram") {
|
||||
} else if (data.type === "telegram") {
|
||||
await testTelegramConnection({
|
||||
botToken: form.getValues("botToken"),
|
||||
chatId: form.getValues("chatId"),
|
||||
messageThreadId: form.getValues("messageThreadId") || "",
|
||||
botToken: data.botToken,
|
||||
chatId: data.chatId,
|
||||
messageThreadId: data.messageThreadId || "",
|
||||
});
|
||||
} else if (type === "discord") {
|
||||
} else if (data.type === "discord") {
|
||||
await testDiscordConnection({
|
||||
webhookUrl: form.getValues("webhookUrl"),
|
||||
decoration: form.getValues("decoration"),
|
||||
webhookUrl: data.webhookUrl,
|
||||
decoration: data.decoration,
|
||||
});
|
||||
} else if (type === "email") {
|
||||
} else if (data.type === "email") {
|
||||
await testEmailConnection({
|
||||
smtpServer: form.getValues("smtpServer"),
|
||||
smtpPort: form.getValues("smtpPort"),
|
||||
username: form.getValues("username"),
|
||||
password: form.getValues("password"),
|
||||
toAddresses: form.getValues("toAddresses"),
|
||||
fromAddress: form.getValues("fromAddress"),
|
||||
smtpServer: data.smtpServer,
|
||||
smtpPort: data.smtpPort,
|
||||
username: data.username,
|
||||
password: data.password,
|
||||
fromAddress: data.fromAddress,
|
||||
toAddresses: data.toAddresses,
|
||||
});
|
||||
} else if (type === "gotify") {
|
||||
} else if (data.type === "gotify") {
|
||||
await testGotifyConnection({
|
||||
serverUrl: form.getValues("serverUrl"),
|
||||
appToken: form.getValues("appToken"),
|
||||
priority: form.getValues("priority"),
|
||||
decoration: form.getValues("decoration"),
|
||||
serverUrl: data.serverUrl,
|
||||
appToken: data.appToken,
|
||||
priority: data.priority,
|
||||
decoration: data.decoration,
|
||||
});
|
||||
} else if (type === "ntfy") {
|
||||
} else if (data.type === "ntfy") {
|
||||
await testNtfyConnection({
|
||||
serverUrl: form.getValues("serverUrl"),
|
||||
topic: form.getValues("topic"),
|
||||
accessToken: form.getValues("accessToken"),
|
||||
priority: form.getValues("priority"),
|
||||
serverUrl: data.serverUrl,
|
||||
topic: data.topic,
|
||||
accessToken: data.accessToken || "",
|
||||
priority: data.priority,
|
||||
});
|
||||
} else if (type === "lark") {
|
||||
} else if (data.type === "lark") {
|
||||
await testLarkConnection({
|
||||
webhookUrl: form.getValues("webhookUrl"),
|
||||
webhookUrl: data.webhookUrl,
|
||||
});
|
||||
}
|
||||
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"}`,
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -52,6 +52,7 @@ const Schema = z.object({
|
||||
sshKeyId: z.string().min(1, {
|
||||
message: "SSH Key is required",
|
||||
}),
|
||||
serverType: z.enum(["deploy", "build"]).default("deploy"),
|
||||
});
|
||||
|
||||
type Schema = z.infer<typeof Schema>;
|
||||
@@ -89,6 +90,7 @@ export const HandleServers = ({ serverId }: Props) => {
|
||||
port: 22,
|
||||
username: "root",
|
||||
sshKeyId: "",
|
||||
serverType: "deploy",
|
||||
},
|
||||
resolver: zodResolver(Schema),
|
||||
});
|
||||
@@ -101,6 +103,7 @@ export const HandleServers = ({ serverId }: Props) => {
|
||||
port: data?.port || 22,
|
||||
username: data?.username || "root",
|
||||
sshKeyId: data?.sshKeyId || "",
|
||||
serverType: data?.serverType || "deploy",
|
||||
});
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
|
||||
|
||||
@@ -116,6 +119,7 @@ export const HandleServers = ({ serverId }: Props) => {
|
||||
port: data.port || 22,
|
||||
username: data.username || "root",
|
||||
sshKeyId: data.sshKeyId || "",
|
||||
serverType: data.serverType || "deploy",
|
||||
serverId: serverId || "",
|
||||
})
|
||||
.then(async (_data) => {
|
||||
@@ -266,6 +270,50 @@ export const HandleServers = ({ serverId }: Props) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="serverType"
|
||||
render={({ field }) => {
|
||||
const serverTypeValue = form.watch("serverType");
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Server Type</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a server type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="deploy">Deploy Server</SelectItem>
|
||||
<SelectItem value="build">Build Server</SelectItem>
|
||||
<SelectLabel>Server Type</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
{serverTypeValue === "deploy" && (
|
||||
<AlertBlock type="info" className="mt-2">
|
||||
Deploy servers are used to run your applications,
|
||||
databases, and services. They handle the deployment and
|
||||
execution of your projects.
|
||||
</AlertBlock>
|
||||
)}
|
||||
{serverTypeValue === "build" && (
|
||||
<AlertBlock type="info" className="mt-2">
|
||||
Build servers are dedicated to building your
|
||||
applications. They handle the compilation and build
|
||||
process, offloading this work from your deployment
|
||||
servers. Build servers won't appear in deployment
|
||||
options.
|
||||
</AlertBlock>
|
||||
)}
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="sshKeyId"
|
||||
|
||||
@@ -51,6 +51,7 @@ export const SetupServer = ({ serverId }: Props) => {
|
||||
|
||||
const [activeLog, setActiveLog] = useState<string | null>(null);
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const isBuildServer = server?.serverType === "build";
|
||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
||||
const [isDeploying, setIsDeploying] = useState(false);
|
||||
@@ -117,17 +118,26 @@ export const SetupServer = ({ serverId }: Props) => {
|
||||
<TabsList
|
||||
className={cn(
|
||||
"grid w-[700px]",
|
||||
isCloud ? "grid-cols-6" : "grid-cols-5",
|
||||
isBuildServer
|
||||
? "grid-cols-3"
|
||||
: isCloud
|
||||
? "grid-cols-6"
|
||||
: "grid-cols-5",
|
||||
)}
|
||||
>
|
||||
<TabsTrigger value="ssh-keys">SSH Keys</TabsTrigger>
|
||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||
<TabsTrigger value="validate">Validate</TabsTrigger>
|
||||
<TabsTrigger value="audit">Security</TabsTrigger>
|
||||
{isCloud && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
|
||||
{!isBuildServer && (
|
||||
<>
|
||||
<TabsTrigger value="audit">Security</TabsTrigger>
|
||||
{isCloud && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="gpu-setup">GPU Setup</TabsTrigger>
|
||||
</>
|
||||
)}
|
||||
<TabsTrigger value="gpu-setup">GPU Setup</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent
|
||||
value="ssh-keys"
|
||||
@@ -311,32 +321,36 @@ export const SetupServer = ({ serverId }: Props) => {
|
||||
<ValidateServer serverId={serverId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent
|
||||
value="audit"
|
||||
className="outline-none ring-0 focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
>
|
||||
<div className="flex flex-col gap-2 text-sm text-muted-foreground pt-3">
|
||||
<SecurityAudit serverId={serverId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent
|
||||
value="monitoring"
|
||||
className="outline-none ring-0 focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
>
|
||||
<div className="flex flex-col gap-2 text-sm pt-3">
|
||||
<div className="rounded-xl bg-background shadow-md border">
|
||||
<SetupMonitoring serverId={serverId} />
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent
|
||||
value="gpu-setup"
|
||||
className="outline-none ring-0 focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
>
|
||||
<div className="flex flex-col gap-2 text-sm text-muted-foreground pt-3">
|
||||
<GPUSupport serverId={serverId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
{!isBuildServer && (
|
||||
<>
|
||||
<TabsContent
|
||||
value="audit"
|
||||
className="outline-none ring-0 focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
>
|
||||
<div className="flex flex-col gap-2 text-sm text-muted-foreground pt-3">
|
||||
<SecurityAudit serverId={serverId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent
|
||||
value="monitoring"
|
||||
className="outline-none ring-0 focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
>
|
||||
<div className="flex flex-col gap-2 text-sm pt-3">
|
||||
<div className="rounded-xl bg-background shadow-md border">
|
||||
<SetupMonitoring serverId={serverId} />
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent
|
||||
value="gpu-setup"
|
||||
className="outline-none ring-0 focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
>
|
||||
<div className="flex flex-col gap-2 text-sm text-muted-foreground pt-3">
|
||||
<GPUSupport serverId={serverId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
</>
|
||||
)}
|
||||
</Tabs>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -129,6 +129,9 @@ export const ShowServers = () => {
|
||||
Status
|
||||
</TableHead>
|
||||
)}
|
||||
<TableHead className="text-center">
|
||||
Type
|
||||
</TableHead>
|
||||
<TableHead className="text-center">
|
||||
IP Address
|
||||
</TableHead>
|
||||
@@ -153,6 +156,8 @@ export const ShowServers = () => {
|
||||
{data?.map((server) => {
|
||||
const canDelete = server.totalSum === 0;
|
||||
const isActive = server.serverStatus === "active";
|
||||
const isBuildServer =
|
||||
server.serverType === "build";
|
||||
return (
|
||||
<TableRow key={server.serverId}>
|
||||
<TableCell className="text-left">
|
||||
@@ -171,6 +176,15 @@ export const ShowServers = () => {
|
||||
</Badge>
|
||||
</TableHead>
|
||||
)}
|
||||
<TableCell className="text-center">
|
||||
<Badge
|
||||
variant={
|
||||
isBuildServer ? "secondary" : "default"
|
||||
}
|
||||
>
|
||||
{server.serverType}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Badge>{server.ipAddress}</Badge>
|
||||
</TableCell>
|
||||
@@ -233,11 +247,12 @@ export const ShowServers = () => {
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
|
||||
{server.sshKeyId && (
|
||||
<ShowServerActions
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
)}
|
||||
{server.sshKeyId &&
|
||||
!isBuildServer && (
|
||||
<ShowServerActions
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -286,41 +301,43 @@ export const ShowServers = () => {
|
||||
</DropdownMenuItem>
|
||||
</DialogAction>
|
||||
|
||||
{isActive && server.sshKeyId && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>
|
||||
Extra
|
||||
</DropdownMenuLabel>
|
||||
{isActive &&
|
||||
server.sshKeyId &&
|
||||
!isBuildServer && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>
|
||||
Extra
|
||||
</DropdownMenuLabel>
|
||||
|
||||
<ShowTraefikFileSystemModal
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
<ShowDockerContainersModal
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
{isCloud && (
|
||||
<ShowMonitoringModal
|
||||
url={`http://${server.ipAddress}:${server?.metricsConfig?.server?.port}/metrics`}
|
||||
token={
|
||||
server?.metricsConfig?.server
|
||||
?.token
|
||||
}
|
||||
<ShowTraefikFileSystemModal
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
)}
|
||||
<ShowDockerContainersModal
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
{isCloud && (
|
||||
<ShowMonitoringModal
|
||||
url={`http://${server.ipAddress}:${server?.metricsConfig?.server?.port}/metrics`}
|
||||
token={
|
||||
server?.metricsConfig
|
||||
?.server?.token
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ShowSwarmOverviewModal
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
<ShowNodesModal
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
<ShowSwarmOverviewModal
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
<ShowNodesModal
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
|
||||
<ShowSchedulesModal
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<ShowSchedulesModal
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
|
||||
@@ -25,6 +25,13 @@ export const ValidateServer = ({ serverId }: Props) => {
|
||||
enabled: !!serverId,
|
||||
},
|
||||
);
|
||||
const { data: server } = api.server.one.useQuery(
|
||||
{ serverId },
|
||||
{
|
||||
enabled: !!serverId,
|
||||
},
|
||||
);
|
||||
const isBuildServer = server?.serverType === "build";
|
||||
const _utils = api.useUtils();
|
||||
return (
|
||||
<CardContent className="p-0">
|
||||
@@ -73,7 +80,9 @@ export const ValidateServer = ({ serverId }: Props) => {
|
||||
<div className="border rounded-lg p-4">
|
||||
<h3 className="text-lg font-semibold mb-1">Status</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
Shows the server configuration status
|
||||
{isBuildServer
|
||||
? "Shows the build server configuration status"
|
||||
: "Shows the server configuration status"}
|
||||
</p>
|
||||
<div className="grid gap-2.5">
|
||||
<StatusRow
|
||||
@@ -85,15 +94,17 @@ export const ValidateServer = ({ serverId }: Props) => {
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<StatusRow
|
||||
label="RClone Installed"
|
||||
isEnabled={data?.rclone?.enabled}
|
||||
description={
|
||||
data?.rclone?.enabled
|
||||
? `Installed: ${data?.rclone?.version}`
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
{!isBuildServer && (
|
||||
<StatusRow
|
||||
label="RClone Installed"
|
||||
isEnabled={data?.rclone?.enabled}
|
||||
description={
|
||||
data?.rclone?.enabled
|
||||
? `Installed: ${data?.rclone?.version}`
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<StatusRow
|
||||
label="Nixpacks Installed"
|
||||
isEnabled={data?.nixpacks?.enabled}
|
||||
@@ -113,23 +124,36 @@ export const ValidateServer = ({ serverId }: Props) => {
|
||||
}
|
||||
/>
|
||||
<StatusRow
|
||||
label="Docker Swarm Initialized"
|
||||
isEnabled={data?.isSwarmInstalled}
|
||||
label="Railpack Installed"
|
||||
isEnabled={data?.railpack?.enabled}
|
||||
description={
|
||||
data?.isSwarmInstalled
|
||||
? "Initialized"
|
||||
: "Not Initialized"
|
||||
}
|
||||
/>
|
||||
<StatusRow
|
||||
label="Dokploy Network Created"
|
||||
isEnabled={data?.isDokployNetworkInstalled}
|
||||
description={
|
||||
data?.isDokployNetworkInstalled
|
||||
? "Created"
|
||||
: "Not Created"
|
||||
data?.railpack?.enabled
|
||||
? `Installed: ${data?.railpack?.version}`
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
{!isBuildServer && (
|
||||
<>
|
||||
<StatusRow
|
||||
label="Docker Swarm Initialized"
|
||||
isEnabled={data?.isSwarmInstalled}
|
||||
description={
|
||||
data?.isSwarmInstalled
|
||||
? "Initialized"
|
||||
: "Not Initialized"
|
||||
}
|
||||
/>
|
||||
<StatusRow
|
||||
label="Dokploy Network Created"
|
||||
isEnabled={data?.isDokployNetworkInstalled}
|
||||
description={
|
||||
data?.isDokployNetworkInstalled
|
||||
? "Created"
|
||||
: "Not Created"
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<StatusRow
|
||||
label="Main Directory Created"
|
||||
isEnabled={data?.isMainDirectoryInstalled}
|
||||
@@ -139,15 +163,6 @@ export const ValidateServer = ({ serverId }: Props) => {
|
||||
: "Not Created"
|
||||
}
|
||||
/>
|
||||
<StatusRow
|
||||
label="Railpack Installed"
|
||||
isEnabled={data?.railpack?.enabled}
|
||||
description={
|
||||
data?.railpack?.enabled
|
||||
? `Installed: ${data?.railpack?.version}`
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -95,6 +95,7 @@ export const CreateServer = ({ stepper }: Props) => {
|
||||
port: data.port || 22,
|
||||
username: data.username || "root",
|
||||
sshKeyId: data.sshKeyId || "",
|
||||
serverType: "deploy",
|
||||
})
|
||||
.then(async (_data) => {
|
||||
toast.success("Server Created");
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCaption,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
@@ -68,7 +67,6 @@ export const ShowUsers = () => {
|
||||
) : (
|
||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||
<Table>
|
||||
<TableCaption>See all users</TableCaption>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[100px]">Email</TableHead>
|
||||
@@ -111,35 +109,75 @@ export const ShowUsers = () => {
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="text-right flex justify-end">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>
|
||||
Actions
|
||||
</DropdownMenuLabel>
|
||||
{member.role !== "owner" && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<span className="sr-only">
|
||||
Open menu
|
||||
</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>
|
||||
Actions
|
||||
</DropdownMenuLabel>
|
||||
|
||||
{member.role !== "owner" && (
|
||||
<AddUserPermissions
|
||||
userId={member.user.id}
|
||||
/>
|
||||
)}
|
||||
|
||||
{member.role !== "owner" && (
|
||||
<>
|
||||
{!isCloud && (
|
||||
<DialogAction
|
||||
title="Delete User"
|
||||
description="Are you sure you want to delete this user?"
|
||||
type="destructive"
|
||||
onClick={async () => {
|
||||
{!isCloud && (
|
||||
<DialogAction
|
||||
title="Delete User"
|
||||
description="Are you sure you want to delete this user?"
|
||||
type="destructive"
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
userId: member.user.id,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success(
|
||||
"User deleted successfully",
|
||||
);
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
"Error deleting destination",
|
||||
);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
Delete User
|
||||
</DropdownMenuItem>
|
||||
</DialogAction>
|
||||
)}
|
||||
|
||||
<DialogAction
|
||||
title="Unlink User"
|
||||
description="Are you sure you want to unlink this user?"
|
||||
type="destructive"
|
||||
onClick={async () => {
|
||||
if (!isCloud) {
|
||||
const orgCount =
|
||||
await utils.user.checkUserOrganizations.fetch(
|
||||
{
|
||||
userId: member.user.id,
|
||||
},
|
||||
);
|
||||
|
||||
console.log(orgCount);
|
||||
|
||||
if (orgCount === 1) {
|
||||
await mutateAsync({
|
||||
userId: member.user.id,
|
||||
})
|
||||
@@ -151,86 +189,40 @@ export const ShowUsers = () => {
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
"Error deleting destination",
|
||||
"Error deleting user",
|
||||
);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
|
||||
onSelect={(e) =>
|
||||
e.preventDefault()
|
||||
}
|
||||
>
|
||||
Delete User
|
||||
</DropdownMenuItem>
|
||||
</DialogAction>
|
||||
)}
|
||||
|
||||
<DialogAction
|
||||
title="Unlink User"
|
||||
description="Are you sure you want to unlink this user?"
|
||||
type="destructive"
|
||||
onClick={async () => {
|
||||
if (!isCloud) {
|
||||
const orgCount =
|
||||
await utils.user.checkUserOrganizations.fetch(
|
||||
{
|
||||
userId: member.user.id,
|
||||
},
|
||||
);
|
||||
|
||||
console.log(orgCount);
|
||||
|
||||
if (orgCount === 1) {
|
||||
await mutateAsync({
|
||||
userId: member.user.id,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success(
|
||||
"User deleted successfully",
|
||||
);
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
"Error deleting user",
|
||||
);
|
||||
});
|
||||
return;
|
||||
}
|
||||
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");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
Unlink User
|
||||
</DropdownMenuItem>
|
||||
</DialogAction>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
Unlink User
|
||||
</DropdownMenuItem>
|
||||
</DialogAction>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
@@ -44,14 +44,20 @@ export function TimeBadge() {
|
||||
.padStart(2, "0")}`;
|
||||
};
|
||||
|
||||
const formattedTime = new Intl.DateTimeFormat("en-US", {
|
||||
timeZone: serverTime.timezone,
|
||||
timeStyle: "medium",
|
||||
hour12: false,
|
||||
}).format(time);
|
||||
|
||||
return (
|
||||
<div className="inline-flex items-center gap-2 rounded-md border px-2 py-1 text-xs sm:text-sm whitespace-nowrap max-w-full overflow-hidden">
|
||||
<span className="hidden sm:inline">Server Time:</span>
|
||||
<span className="font-medium tabular-nums">
|
||||
{time.toLocaleTimeString()}
|
||||
</span>
|
||||
<span className="hidden sm:inline text-muted-foreground">
|
||||
({serverTime.timezone} | {getUtcOffset(serverTime.timezone)})
|
||||
<div className="inline-flex items-center rounded-full border p-1 text-xs whitespace-nowrap max-w-full overflow-hidden gap-1">
|
||||
<div className="inline-flex items-center px-1 gap-1">
|
||||
<span className="hidden sm:inline">Server Time:</span>
|
||||
<span className="font-medium tabular-nums">{formattedTime}</span>
|
||||
</div>
|
||||
<span className="hidden sm:inline text-primary/70 border rounded-full bg-foreground/5 px-1.5 py-0.5">
|
||||
{serverTime.timezone} | {getUtcOffset(serverTime.timezone)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
9
apps/dokploy/drizzle/0121_rainy_cargill.sql
Normal file
9
apps/dokploy/drizzle/0121_rainy_cargill.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
-- Fix inconsistent date formats in environment.createdAt field
|
||||
-- Convert PostgreSQL timestamp format to ISO 8601 format
|
||||
-- This addresses issue #2992 where old environments have PostgreSQL timestamp format
|
||||
-- while new ones have ISO 8601 format
|
||||
|
||||
UPDATE "environment"
|
||||
SET "createdAt" = to_char("createdAt"::timestamptz, 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')
|
||||
WHERE "createdAt" NOT LIKE '%T%';
|
||||
|
||||
8
apps/dokploy/drizzle/0122_absent_frightful_four.sql
Normal file
8
apps/dokploy/drizzle/0122_absent_frightful_four.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
CREATE TYPE "public"."serverType" AS ENUM('deploy', 'build');--> statement-breakpoint
|
||||
ALTER TABLE "application" ADD COLUMN "buildServerId" text;--> statement-breakpoint
|
||||
ALTER TABLE "application" ADD COLUMN "buildRegistryId" text;--> statement-breakpoint
|
||||
ALTER TABLE "deployment" ADD COLUMN "buildServerId" text;--> statement-breakpoint
|
||||
ALTER TABLE "server" ADD COLUMN "serverType" "serverType" DEFAULT 'deploy' NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "application" ADD CONSTRAINT "application_buildServerId_server_serverId_fk" FOREIGN KEY ("buildServerId") REFERENCES "public"."server"("serverId") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "application" ADD CONSTRAINT "application_buildRegistryId_registry_registryId_fk" FOREIGN KEY ("buildRegistryId") REFERENCES "public"."registry"("registryId") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "deployment" ADD CONSTRAINT "deployment_buildServerId_server_serverId_fk" FOREIGN KEY ("buildServerId") REFERENCES "public"."server"("serverId") ON DELETE cascade ON UPDATE no action;
|
||||
6
apps/dokploy/drizzle/0123_cloudy_piledriver.sql
Normal file
6
apps/dokploy/drizzle/0123_cloudy_piledriver.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE "application" ADD COLUMN "args" text[];--> statement-breakpoint
|
||||
ALTER TABLE "mariadb" ADD COLUMN "args" text[];--> statement-breakpoint
|
||||
ALTER TABLE "mongo" ADD COLUMN "args" text[];--> statement-breakpoint
|
||||
ALTER TABLE "mysql" ADD COLUMN "args" text[];--> statement-breakpoint
|
||||
ALTER TABLE "postgres" ADD COLUMN "args" text[];--> statement-breakpoint
|
||||
ALTER TABLE "redis" ADD COLUMN "args" text[];
|
||||
1
apps/dokploy/drizzle/0124_certain_cloak.sql
Normal file
1
apps/dokploy/drizzle/0124_certain_cloak.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "ntfy" ALTER COLUMN "accessToken" DROP NOT NULL;
|
||||
6722
apps/dokploy/drizzle/meta/0121_snapshot.json
Normal file
6722
apps/dokploy/drizzle/meta/0121_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
6795
apps/dokploy/drizzle/meta/0122_snapshot.json
Normal file
6795
apps/dokploy/drizzle/meta/0122_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
6831
apps/dokploy/drizzle/meta/0123_snapshot.json
Normal file
6831
apps/dokploy/drizzle/meta/0123_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
6831
apps/dokploy/drizzle/meta/0124_snapshot.json
Normal file
6831
apps/dokploy/drizzle/meta/0124_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -848,6 +848,34 @@
|
||||
"when": 1762632540024,
|
||||
"tag": "0120_lame_captain_midlands",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 121,
|
||||
"version": "7",
|
||||
"when": 1763755037033,
|
||||
"tag": "0121_rainy_cargill",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 122,
|
||||
"version": "7",
|
||||
"when": 1764479387555,
|
||||
"tag": "0122_absent_frightful_four",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 123,
|
||||
"version": "7",
|
||||
"when": 1764525308939,
|
||||
"tag": "0123_cloudy_piledriver",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 124,
|
||||
"version": "7",
|
||||
"when": 1764571454170,
|
||||
"tag": "0124_certain_cloak",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dokploy",
|
||||
"version": "v0.25.8",
|
||||
"version": "v0.25.11",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
@@ -34,7 +34,8 @@
|
||||
"docker:build:canary": "./docker/build.sh canary",
|
||||
"docker:push:canary": "./docker/push.sh canary",
|
||||
"version": "echo $(node -p \"require('./package.json').version\")",
|
||||
"test": "vitest --config __test__/vitest.config.ts"
|
||||
"test": "vitest --config __test__/vitest.config.ts",
|
||||
"generate:openapi": "tsx -r dotenv/config scripts/generate-openapi.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "^2.0.5",
|
||||
@@ -98,6 +99,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",
|
||||
@@ -112,7 +114,6 @@
|
||||
"i18next": "^23.16.8",
|
||||
"input-otp": "^1.4.2",
|
||||
"js-cookie": "^3.0.5",
|
||||
"yaml": "2.8.1",
|
||||
"lodash": "4.17.21",
|
||||
"lucide-react": "^0.469.0",
|
||||
"micromatch": "4.0.8",
|
||||
@@ -120,6 +121,7 @@
|
||||
"next": "^15.3.2",
|
||||
"next-i18next": "^15.4.2",
|
||||
"next-themes": "^0.2.1",
|
||||
"nextjs-toploader": "^3.9.17",
|
||||
"node-os-utils": "2.0.1",
|
||||
"node-pty": "1.0.0",
|
||||
"node-schedule": "2.1.1",
|
||||
@@ -153,10 +155,12 @@
|
||||
"use-resize-observer": "9.1.0",
|
||||
"ws": "8.16.0",
|
||||
"xterm-addon-fit": "^0.8.0",
|
||||
"yaml": "2.8.1",
|
||||
"zod": "^3.25.32",
|
||||
"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",
|
||||
|
||||
@@ -7,6 +7,7 @@ import Head from "next/head";
|
||||
import Script from "next/script";
|
||||
import { appWithTranslation } from "next-i18next";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import NextTopLoader from "nextjs-toploader";
|
||||
import type { ReactElement, ReactNode } from "react";
|
||||
import { SearchCommand } from "@/components/dashboard/search-command";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
@@ -57,6 +58,7 @@ const MyApp = ({
|
||||
disableTransitionOnChange
|
||||
forcedTheme={Component.theme}
|
||||
>
|
||||
<NextTopLoader color="hsl(var(--sidebar-ring))" />
|
||||
<Toaster richColors />
|
||||
<SearchCommand />
|
||||
{getLayout(<Component {...pageProps} />)}
|
||||
|
||||
@@ -17,6 +17,7 @@ import { AddCommand } from "@/components/dashboard/application/advanced/general/
|
||||
import { ShowPorts } from "@/components/dashboard/application/advanced/ports/show-port";
|
||||
import { ShowRedirects } from "@/components/dashboard/application/advanced/redirects/show-redirects";
|
||||
import { ShowSecurity } from "@/components/dashboard/application/advanced/security/show-security";
|
||||
import { ShowBuildServer } from "@/components/dashboard/application/advanced/show-build-server";
|
||||
import { ShowResources } from "@/components/dashboard/application/advanced/show-resources";
|
||||
import { ShowTraefikConfig } from "@/components/dashboard/application/advanced/traefik/show-traefik-config";
|
||||
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
|
||||
@@ -353,7 +354,7 @@ const Service = (
|
||||
id={applicationId}
|
||||
type="application"
|
||||
/>
|
||||
|
||||
<ShowBuildServer applicationId={applicationId} />
|
||||
<ShowResources id={applicationId} type="application" />
|
||||
<ShowVolumes id={applicationId} type="application" />
|
||||
<ShowRedirects applicationId={applicationId} />
|
||||
|
||||
132
apps/dokploy/scripts/generate-openapi.ts
Normal file
132
apps/dokploy/scripts/generate-openapi.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
/**
|
||||
* Script to generate OpenAPI specification locally
|
||||
* This runs in CI/CD to generate the openapi.json file
|
||||
* which can then be consumed by the documentation website
|
||||
*/
|
||||
|
||||
import { writeFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
|
||||
import { appRouter } from "../server/api/root";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
async function generateOpenAPI() {
|
||||
try {
|
||||
console.log("🔄 Generating OpenAPI specification...");
|
||||
|
||||
const openApiDocument = generateOpenApiDocument(appRouter, {
|
||||
title: "Dokploy API",
|
||||
version: "1.0.0",
|
||||
baseUrl: "https://your-dokploy-instance.com/api",
|
||||
docsUrl: "https://docs.dokploy.com/api",
|
||||
tags: [
|
||||
"admin",
|
||||
"docker",
|
||||
"compose",
|
||||
"registry",
|
||||
"cluster",
|
||||
"user",
|
||||
"domain",
|
||||
"destination",
|
||||
"backup",
|
||||
"deployment",
|
||||
"mounts",
|
||||
"certificates",
|
||||
"settings",
|
||||
"security",
|
||||
"redirects",
|
||||
"port",
|
||||
"project",
|
||||
"application",
|
||||
"mysql",
|
||||
"postgres",
|
||||
"redis",
|
||||
"mongo",
|
||||
"mariadb",
|
||||
"sshRouter",
|
||||
"gitProvider",
|
||||
"bitbucket",
|
||||
"github",
|
||||
"gitlab",
|
||||
"gitea",
|
||||
"server",
|
||||
"swarm",
|
||||
"ai",
|
||||
"organization",
|
||||
"schedule",
|
||||
"rollback",
|
||||
"volumeBackups",
|
||||
"environment",
|
||||
],
|
||||
});
|
||||
|
||||
// Enhance metadata
|
||||
openApiDocument.info = {
|
||||
title: "Dokploy API",
|
||||
description:
|
||||
"Complete API documentation for Dokploy - Deploy applications, manage databases, and orchestrate your infrastructure. This API allows you to programmatically manage all aspects of your Dokploy instance.",
|
||||
version: "1.0.0",
|
||||
contact: {
|
||||
name: "Dokploy Team",
|
||||
url: "https://dokploy.com",
|
||||
},
|
||||
license: {
|
||||
name: "Apache 2.0",
|
||||
url: "https://github.com/dokploy/dokploy/blob/canary/LICENSE",
|
||||
},
|
||||
};
|
||||
|
||||
// Add security schemes
|
||||
openApiDocument.components = {
|
||||
...openApiDocument.components,
|
||||
securitySchemes: {
|
||||
apiKey: {
|
||||
type: "apiKey",
|
||||
in: "header",
|
||||
name: "x-api-key",
|
||||
description:
|
||||
"API key authentication. Generate an API key from your Dokploy dashboard under Settings > API Keys.",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Apply global security
|
||||
openApiDocument.security = [
|
||||
{
|
||||
apiKey: [],
|
||||
},
|
||||
];
|
||||
|
||||
// Add external docs
|
||||
openApiDocument.externalDocs = {
|
||||
description: "Full documentation",
|
||||
url: "https://docs.dokploy.com",
|
||||
};
|
||||
|
||||
// Write to root of repo
|
||||
const outputPath = resolve(__dirname, "../../../openapi.json");
|
||||
writeFileSync(
|
||||
outputPath,
|
||||
JSON.stringify(openApiDocument, null, 2),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
console.log("✅ OpenAPI specification generated successfully!");
|
||||
console.log(`📄 Output: ${outputPath}`);
|
||||
console.log(
|
||||
`📊 Endpoints: ${Object.keys(openApiDocument.paths || {}).length}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("❌ Error generating OpenAPI specification:", error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
generateOpenAPI();
|
||||
@@ -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}"`;
|
||||
|
||||
@@ -111,7 +111,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,
|
||||
});
|
||||
}
|
||||
@@ -228,7 +228,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,
|
||||
});
|
||||
}
|
||||
@@ -285,7 +285,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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -81,8 +81,10 @@ export const serverRouter = createTRPCRouter({
|
||||
}),
|
||||
getDefaultCommand: protectedProcedure
|
||||
.input(apiFindOneServer)
|
||||
.query(async () => {
|
||||
return defaultCommand();
|
||||
.query(async ({ input }) => {
|
||||
const server = await findServerById(input.serverId);
|
||||
const isBuildServer = server.serverType === "build";
|
||||
return defaultCommand(isBuildServer);
|
||||
}),
|
||||
all: protectedProcedure.query(async ({ ctx }) => {
|
||||
const result = await db
|
||||
@@ -124,10 +126,30 @@ export const serverRouter = createTRPCRouter({
|
||||
isNotNull(server.sshKeyId),
|
||||
eq(server.organizationId, ctx.session.activeOrganizationId),
|
||||
eq(server.serverStatus, "active"),
|
||||
eq(server.serverType, "deploy"),
|
||||
)
|
||||
: and(
|
||||
isNotNull(server.sshKeyId),
|
||||
eq(server.organizationId, ctx.session.activeOrganizationId),
|
||||
eq(server.serverType, "deploy"),
|
||||
),
|
||||
});
|
||||
return result;
|
||||
}),
|
||||
buildServers: protectedProcedure.query(async ({ ctx }) => {
|
||||
const result = await db.query.server.findMany({
|
||||
orderBy: desc(server.createdAt),
|
||||
where: IS_CLOUD
|
||||
? and(
|
||||
isNotNull(server.sshKeyId),
|
||||
eq(server.organizationId, ctx.session.activeOrganizationId),
|
||||
eq(server.serverStatus, "active"),
|
||||
eq(server.serverType, "build"),
|
||||
)
|
||||
: and(
|
||||
isNotNull(server.sshKeyId),
|
||||
eq(server.organizationId, ctx.session.activeOrganizationId),
|
||||
eq(server.serverType, "build"),
|
||||
),
|
||||
});
|
||||
return result;
|
||||
|
||||
@@ -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();
|
||||
}),
|
||||
|
||||
|
||||
@@ -2,7 +2,13 @@ import { z } from "zod";
|
||||
|
||||
export const domain = z
|
||||
.object({
|
||||
host: z.string().min(1, { message: "Add a hostname" }),
|
||||
host: z
|
||||
.string()
|
||||
.min(1, { message: "Add a hostname" })
|
||||
.refine((val) => val === val.trim(), {
|
||||
message: "Domain name cannot have leading or trailing spaces",
|
||||
})
|
||||
.transform((val) => val.trim()),
|
||||
path: z.string().min(1).optional(),
|
||||
port: z
|
||||
.number()
|
||||
@@ -33,7 +39,13 @@ export const domain = z
|
||||
|
||||
export const domainCompose = z
|
||||
.object({
|
||||
host: z.string().min(1, { message: "Host is required" }),
|
||||
host: z
|
||||
.string()
|
||||
.min(1, { message: "Add a hostname" })
|
||||
.refine((val) => val === val.trim(), {
|
||||
message: "Domain name cannot have leading or trailing spaces",
|
||||
})
|
||||
.transform((val) => val.trim()),
|
||||
path: z.string().min(1).optional(),
|
||||
port: z
|
||||
.number()
|
||||
|
||||
@@ -46,6 +46,14 @@ export const setupDockerContainerLogsWebSocketServer = (
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Set up keep-alive ping mechanism to prevent timeout
|
||||
// Send ping every 45 seconds to keep connection alive
|
||||
const pingInterval = setInterval(() => {
|
||||
if (ws.readyState === ws.OPEN) {
|
||||
ws.ping();
|
||||
}
|
||||
}, 45000); // 45 seconds
|
||||
try {
|
||||
if (serverId) {
|
||||
const server = await findServerById(serverId);
|
||||
@@ -86,6 +94,7 @@ export const setupDockerContainerLogsWebSocketServer = (
|
||||
.on("error", (err) => {
|
||||
console.error("SSH connection error:", err);
|
||||
ws.send(`SSH error: ${err.message}`);
|
||||
clearInterval(pingInterval);
|
||||
ws.close(); // Cierra el WebSocket si hay un error con SSH
|
||||
client.end();
|
||||
})
|
||||
@@ -96,6 +105,7 @@ export const setupDockerContainerLogsWebSocketServer = (
|
||||
privateKey: server.sshKey?.privateKey,
|
||||
});
|
||||
ws.on("close", () => {
|
||||
clearInterval(pingInterval);
|
||||
client.end();
|
||||
});
|
||||
} else {
|
||||
@@ -121,6 +131,7 @@ export const setupDockerContainerLogsWebSocketServer = (
|
||||
ws.send(data);
|
||||
});
|
||||
ws.on("close", () => {
|
||||
clearInterval(pingInterval);
|
||||
ptyProcess.kill();
|
||||
});
|
||||
ws.on("message", (message) => {
|
||||
|
||||
20004
openapi.json
Normal file
20004
openapi.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,8 @@
|
||||
"build": "pnpm -r run build",
|
||||
"format-and-lint": "biome check .",
|
||||
"check": "biome check --write --no-errors-on-unmatched --files-ignore-unknown=true",
|
||||
"format-and-lint:fix": "biome check . --write"
|
||||
"format-and-lint:fix": "biome check . --write",
|
||||
"generate:openapi": "pnpm --filter=dokploy run generate:openapi"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.1.1",
|
||||
|
||||
@@ -75,6 +75,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",
|
||||
@@ -93,6 +94,7 @@
|
||||
"@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",
|
||||
|
||||
@@ -111,6 +111,7 @@ export const applications = pgTable("application", {
|
||||
enabled: boolean("enabled"),
|
||||
subtitle: text("subtitle"),
|
||||
command: text("command"),
|
||||
args: text("args").array(),
|
||||
refreshToken: text("refreshToken").$defaultFn(() => nanoid()),
|
||||
sourceType: sourceType("sourceType").notNull().default("github"),
|
||||
cleanCache: boolean("cleanCache").default(false),
|
||||
@@ -204,6 +205,15 @@ export const applications = pgTable("application", {
|
||||
serverId: text("serverId").references(() => server.serverId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
buildServerId: text("buildServerId").references(() => server.serverId, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
buildRegistryId: text("buildRegistryId").references(
|
||||
() => registry.registryId,
|
||||
{
|
||||
onDelete: "set null",
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
export const applicationsRelations = relations(
|
||||
@@ -226,6 +236,7 @@ export const applicationsRelations = relations(
|
||||
registry: one(registry, {
|
||||
fields: [applications.registryId],
|
||||
references: [registry.registryId],
|
||||
relationName: "applicationRegistry",
|
||||
}),
|
||||
github: one(github, {
|
||||
fields: [applications.githubId],
|
||||
@@ -246,6 +257,17 @@ export const applicationsRelations = relations(
|
||||
server: one(server, {
|
||||
fields: [applications.serverId],
|
||||
references: [server.serverId],
|
||||
relationName: "applicationServer",
|
||||
}),
|
||||
buildServer: one(server, {
|
||||
fields: [applications.buildServerId],
|
||||
references: [server.serverId],
|
||||
relationName: "applicationBuildServer",
|
||||
}),
|
||||
buildRegistry: one(registry, {
|
||||
fields: [applications.buildRegistryId],
|
||||
references: [registry.registryId],
|
||||
relationName: "applicationBuildRegistry",
|
||||
}),
|
||||
previewDeployments: many(previewDeployments),
|
||||
}),
|
||||
@@ -272,6 +294,7 @@ const createSchema = createInsertSchema(applications, {
|
||||
username: z.string().optional(),
|
||||
isPreviewDeploymentsActive: z.boolean().optional(),
|
||||
password: z.string().optional(),
|
||||
args: z.array(z.string()).optional(),
|
||||
registryUrl: z.string().optional(),
|
||||
customGitSSHKeyId: z.string().optional(),
|
||||
repository: z.string().optional(),
|
||||
|
||||
@@ -70,6 +70,9 @@ export const deployments = pgTable("deployment", {
|
||||
(): AnyPgColumn => volumeBackups.volumeBackupId,
|
||||
{ onDelete: "cascade" },
|
||||
),
|
||||
buildServerId: text("buildServerId").references(() => server.serverId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
});
|
||||
|
||||
export const deploymentsRelations = relations(deployments, ({ one }) => ({
|
||||
@@ -84,6 +87,12 @@ export const deploymentsRelations = relations(deployments, ({ one }) => ({
|
||||
server: one(server, {
|
||||
fields: [deployments.serverId],
|
||||
references: [server.serverId],
|
||||
relationName: "deploymentServer",
|
||||
}),
|
||||
buildServer: one(server, {
|
||||
fields: [deployments.buildServerId],
|
||||
references: [server.serverId],
|
||||
relationName: "deploymentBuildServer",
|
||||
}),
|
||||
previewDeployment: one(previewDeployments, {
|
||||
fields: [deployments.previewDeploymentId],
|
||||
@@ -115,6 +124,7 @@ const schema = createInsertSchema(deployments, {
|
||||
composeId: z.string(),
|
||||
description: z.string().optional(),
|
||||
previewDeploymentId: z.string(),
|
||||
buildServerId: z.string(),
|
||||
});
|
||||
|
||||
export const apiCreateDeployment = schema
|
||||
|
||||
@@ -45,6 +45,7 @@ export const mariadb = pgTable("mariadb", {
|
||||
databaseRootPassword: text("rootPassword").notNull(),
|
||||
dockerImage: text("dockerImage").notNull(),
|
||||
command: text("command"),
|
||||
args: text("args").array(),
|
||||
env: text("env"),
|
||||
// RESOURCES
|
||||
memoryReservation: text("memoryReservation"),
|
||||
@@ -114,6 +115,7 @@ const createSchema = createInsertSchema(mariadb, {
|
||||
.optional(),
|
||||
dockerImage: z.string().default("mariadb:6"),
|
||||
command: z.string().optional(),
|
||||
args: z.array(z.string()).optional(),
|
||||
env: z.string().optional(),
|
||||
memoryReservation: z.string().optional(),
|
||||
memoryLimit: z.string().optional(),
|
||||
|
||||
@@ -50,6 +50,7 @@ export const mongo = pgTable("mongo", {
|
||||
databasePassword: text("databasePassword").notNull(),
|
||||
dockerImage: text("dockerImage").notNull(),
|
||||
command: text("command"),
|
||||
args: text("args").array(),
|
||||
env: text("env"),
|
||||
memoryReservation: text("memoryReservation"),
|
||||
memoryLimit: text("memoryLimit"),
|
||||
@@ -110,6 +111,7 @@ const createSchema = createInsertSchema(mongo, {
|
||||
databaseUser: z.string().min(1),
|
||||
dockerImage: z.string().default("mongo:15"),
|
||||
command: z.string().optional(),
|
||||
args: z.array(z.string()).optional(),
|
||||
env: z.string().optional(),
|
||||
memoryReservation: z.string().optional(),
|
||||
memoryLimit: z.string().optional(),
|
||||
|
||||
@@ -45,6 +45,7 @@ export const mysql = pgTable("mysql", {
|
||||
databaseRootPassword: text("rootPassword").notNull(),
|
||||
dockerImage: text("dockerImage").notNull(),
|
||||
command: text("command"),
|
||||
args: text("args").array(),
|
||||
env: text("env"),
|
||||
memoryReservation: text("memoryReservation"),
|
||||
memoryLimit: text("memoryLimit"),
|
||||
@@ -112,6 +113,7 @@ const createSchema = createInsertSchema(mysql, {
|
||||
.optional(),
|
||||
dockerImage: z.string().default("mysql:8"),
|
||||
command: z.string().optional(),
|
||||
args: z.array(z.string()).optional(),
|
||||
env: z.string().optional(),
|
||||
memoryReservation: z.string().optional(),
|
||||
memoryLimit: z.string().optional(),
|
||||
|
||||
@@ -116,7 +116,7 @@ export const ntfy = pgTable("ntfy", {
|
||||
.$defaultFn(() => nanoid()),
|
||||
serverUrl: text("serverUrl").notNull(),
|
||||
topic: text("topic").notNull(),
|
||||
accessToken: text("accessToken").notNull(),
|
||||
accessToken: text("accessToken"),
|
||||
priority: integer("priority").notNull().default(3),
|
||||
});
|
||||
|
||||
@@ -331,7 +331,7 @@ export const apiCreateNtfy = notificationsSchema
|
||||
.extend({
|
||||
serverUrl: z.string().min(1),
|
||||
topic: z.string().min(1),
|
||||
accessToken: z.string().min(1),
|
||||
accessToken: z.string().optional(),
|
||||
priority: z.number().min(1),
|
||||
})
|
||||
.required();
|
||||
@@ -395,7 +395,7 @@ export const apiSendTest = notificationsSchema
|
||||
serverUrl: z.string(),
|
||||
topic: z.string(),
|
||||
appToken: z.string(),
|
||||
accessToken: z.string(),
|
||||
accessToken: z.string().optional(),
|
||||
priority: z.number(),
|
||||
})
|
||||
.partial();
|
||||
|
||||
@@ -44,6 +44,7 @@ export const postgres = pgTable("postgres", {
|
||||
description: text("description"),
|
||||
dockerImage: text("dockerImage").notNull(),
|
||||
command: text("command"),
|
||||
args: text("args").array(),
|
||||
env: text("env"),
|
||||
memoryReservation: text("memoryReservation"),
|
||||
externalPort: integer("externalPort"),
|
||||
@@ -103,6 +104,7 @@ const createSchema = createInsertSchema(postgres, {
|
||||
databaseUser: z.string().min(1),
|
||||
dockerImage: z.string().default("postgres:15"),
|
||||
command: z.string().optional(),
|
||||
args: z.array(z.string()).optional(),
|
||||
env: z.string().optional(),
|
||||
memoryReservation: z.string().optional(),
|
||||
memoryLimit: z.string().optional(),
|
||||
|
||||
@@ -41,6 +41,7 @@ export const redis = pgTable("redis", {
|
||||
databasePassword: text("password").notNull(),
|
||||
dockerImage: text("dockerImage").notNull(),
|
||||
command: text("command"),
|
||||
args: text("args").array(),
|
||||
env: text("env"),
|
||||
memoryReservation: text("memoryReservation"),
|
||||
memoryLimit: text("memoryLimit"),
|
||||
@@ -93,6 +94,7 @@ const createSchema = createInsertSchema(redis, {
|
||||
databasePassword: z.string(),
|
||||
dockerImage: z.string().default("redis:8"),
|
||||
command: z.string().optional(),
|
||||
args: z.array(z.string()).optional(),
|
||||
env: z.string().optional(),
|
||||
memoryReservation: z.string().optional(),
|
||||
memoryLimit: z.string().optional(),
|
||||
|
||||
@@ -33,7 +33,12 @@ export const registry = pgTable("registry", {
|
||||
});
|
||||
|
||||
export const registryRelations = relations(registry, ({ many }) => ({
|
||||
applications: many(applications),
|
||||
applications: many(applications, {
|
||||
relationName: "applicationRegistry",
|
||||
}),
|
||||
buildApplications: many(applications, {
|
||||
relationName: "applicationBuildRegistry",
|
||||
}),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(registry, {
|
||||
|
||||
@@ -24,6 +24,7 @@ import { schedules } from "./schedule";
|
||||
import { sshKeys } from "./ssh-key";
|
||||
import { generateAppName } from "./utils";
|
||||
export const serverStatus = pgEnum("serverStatus", ["active", "inactive"]);
|
||||
export const serverType = pgEnum("serverType", ["deploy", "build"]);
|
||||
|
||||
export const server = pgTable("server", {
|
||||
serverId: text("serverId")
|
||||
@@ -44,6 +45,7 @@ export const server = pgTable("server", {
|
||||
.notNull()
|
||||
.references(() => organization.id, { onDelete: "cascade" }),
|
||||
serverStatus: serverStatus("serverStatus").notNull().default("active"),
|
||||
serverType: serverType("serverType").notNull().default("deploy"),
|
||||
command: text("command").notNull().default(""),
|
||||
sshKeyId: text("sshKeyId").references(() => sshKeys.sshKeyId, {
|
||||
onDelete: "set null",
|
||||
@@ -97,12 +99,22 @@ export const server = pgTable("server", {
|
||||
});
|
||||
|
||||
export const serverRelations = relations(server, ({ one, many }) => ({
|
||||
deployments: many(deployments),
|
||||
deployments: many(deployments, {
|
||||
relationName: "deploymentServer",
|
||||
}),
|
||||
buildDeployments: many(deployments, {
|
||||
relationName: "deploymentBuildServer",
|
||||
}),
|
||||
sshKey: one(sshKeys, {
|
||||
fields: [server.sshKeyId],
|
||||
references: [sshKeys.sshKeyId],
|
||||
}),
|
||||
applications: many(applications),
|
||||
applications: many(applications, {
|
||||
relationName: "applicationServer",
|
||||
}),
|
||||
buildApplications: many(applications, {
|
||||
relationName: "applicationBuildServer",
|
||||
}),
|
||||
compose: many(compose),
|
||||
redis: many(redis),
|
||||
mariadb: many(mariadb),
|
||||
@@ -131,6 +143,7 @@ export const apiCreateServer = createSchema
|
||||
port: true,
|
||||
username: true,
|
||||
sshKeyId: true,
|
||||
serverType: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
@@ -155,6 +168,7 @@ export const apiUpdateServer = createSchema
|
||||
port: true,
|
||||
username: true,
|
||||
sshKeyId: true,
|
||||
serverType: true,
|
||||
})
|
||||
.required()
|
||||
.extend({
|
||||
|
||||
@@ -2,7 +2,13 @@ import { z } from "zod";
|
||||
|
||||
export const domain = z
|
||||
.object({
|
||||
host: z.string().min(1, { message: "Add a hostname" }),
|
||||
host: z
|
||||
.string()
|
||||
.min(1, { message: "Add a hostname" })
|
||||
.refine((val) => val === val.trim(), {
|
||||
message: "Domain name cannot have leading or trailing spaces",
|
||||
})
|
||||
.transform((val) => val.trim()),
|
||||
path: z.string().min(1).optional(),
|
||||
internalPath: z.string().optional(),
|
||||
stripPath: z.boolean().optional(),
|
||||
@@ -58,7 +64,13 @@ export const domain = z
|
||||
|
||||
export const domainCompose = z
|
||||
.object({
|
||||
host: z.string().min(1, { message: "Host is required" }),
|
||||
host: z
|
||||
.string()
|
||||
.min(1, { message: "Add a hostname" })
|
||||
.refine((val) => val === val.trim(), {
|
||||
message: "Domain name cannot have leading or trailing spaces",
|
||||
})
|
||||
.transform((val) => val.trim()),
|
||||
path: z.string().min(1).optional(),
|
||||
internalPath: z.string().optional(),
|
||||
stripPath: z.boolean().optional(),
|
||||
|
||||
@@ -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 = ({
|
||||
<Text className="!leading-3">
|
||||
Application Name: <strong>{applicationName}</strong>
|
||||
</Text>
|
||||
<Text className="!leading-3">
|
||||
Environment: <strong>{environmentName}</strong>
|
||||
</Text>
|
||||
<Text className="!leading-3">
|
||||
Application Type: <strong>{applicationType}</strong>
|
||||
</Text>
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
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";
|
||||
@@ -28,6 +29,7 @@ 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";
|
||||
import { encodeBase64 } from "../utils/docker/utils";
|
||||
import { getDokployUrl } from "./admin";
|
||||
import {
|
||||
createDeployment,
|
||||
@@ -110,6 +112,7 @@ export const findApplicationById = async (applicationId: string) => {
|
||||
gitea: true,
|
||||
server: true,
|
||||
previewDeployments: true,
|
||||
buildRegistry: true,
|
||||
},
|
||||
});
|
||||
if (!application) {
|
||||
@@ -170,6 +173,7 @@ export const deployApplication = async ({
|
||||
descriptionLog: string;
|
||||
}) => {
|
||||
const application = await findApplicationById(applicationId);
|
||||
const serverId = application.buildServerId || application.serverId;
|
||||
|
||||
const buildLink = `${await getDokployUrl()}/dashboard/project/${application.environment.projectId}/environment/${application.environmentId}/services/application/${application.applicationId}?tab=deployments`;
|
||||
const deployment = await createDeployment({
|
||||
@@ -197,8 +201,8 @@ export const deployApplication = async ({
|
||||
command += getBuildCommand(application);
|
||||
|
||||
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
|
||||
if (application.serverId) {
|
||||
await execAsyncRemote(application.serverId, commandWithLog);
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, commandWithLog);
|
||||
} else {
|
||||
await execAsync(commandWithLog);
|
||||
}
|
||||
@@ -225,11 +229,21 @@ export const deployApplication = async ({
|
||||
buildLink,
|
||||
organizationId: application.environment.project.organizationId,
|
||||
domains: application.domains,
|
||||
environmentName: application.environment.name,
|
||||
});
|
||||
} catch (error) {
|
||||
const command = `echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath};`;
|
||||
if (application.serverId) {
|
||||
await execAsyncRemote(application.serverId, command);
|
||||
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 (serverId) {
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
@@ -273,6 +287,8 @@ export const rebuildApplication = async ({
|
||||
descriptionLog: string;
|
||||
}) => {
|
||||
const application = await findApplicationById(applicationId);
|
||||
const serverId = application.buildServerId || application.serverId;
|
||||
const buildLink = `${await getDokployUrl()}/dashboard/project/${application.environment.projectId}/environment/${application.environmentId}/services/application/${application.applicationId}?tab=deployments`;
|
||||
|
||||
const deployment = await createDeployment({
|
||||
applicationId: applicationId,
|
||||
@@ -285,15 +301,51 @@ export const rebuildApplication = async ({
|
||||
// Check case for docker only
|
||||
command += getBuildCommand(application);
|
||||
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
|
||||
if (application.serverId) {
|
||||
await execAsyncRemote(application.serverId, commandWithLog);
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, commandWithLog);
|
||||
} else {
|
||||
await execAsync(commandWithLog);
|
||||
}
|
||||
await mechanizeDockerContainer(application);
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updateApplicationStatus(applicationId, "done");
|
||||
|
||||
if (application.rollbackActive) {
|
||||
const tagImage =
|
||||
application.sourceType === "docker"
|
||||
? application.dockerImage
|
||||
: application.appName;
|
||||
await createRollback({
|
||||
appName: tagImage || "",
|
||||
deploymentId: deployment.deploymentId,
|
||||
});
|
||||
}
|
||||
|
||||
await sendBuildSuccessNotifications({
|
||||
projectName: application.environment.project.name,
|
||||
applicationName: application.name,
|
||||
applicationType: "application",
|
||||
buildLink,
|
||||
organizationId: application.environment.project.organizationId,
|
||||
domains: application.domains,
|
||||
environmentName: application.environment.name,
|
||||
});
|
||||
} catch (error) {
|
||||
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 (serverId) {
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
await updateApplicationStatus(applicationId, "error");
|
||||
throw error;
|
||||
|
||||
@@ -78,7 +78,6 @@ const BUNNY_CDN_IPS = new Set([
|
||||
"89.187.188.227",
|
||||
"89.187.188.228",
|
||||
"139.180.134.196",
|
||||
"89.38.96.158",
|
||||
"89.187.162.249",
|
||||
"89.187.162.242",
|
||||
"185.102.217.65",
|
||||
@@ -106,12 +105,9 @@ const BUNNY_CDN_IPS = new Set([
|
||||
"200.25.38.69",
|
||||
"200.25.42.70",
|
||||
"200.25.36.166",
|
||||
"195.206.229.106",
|
||||
"194.242.11.186",
|
||||
"185.164.35.8",
|
||||
"94.20.154.22",
|
||||
"185.93.1.244",
|
||||
"156.59.145.154",
|
||||
"143.244.49.177",
|
||||
"138.199.46.66",
|
||||
"138.199.37.227",
|
||||
@@ -136,7 +132,6 @@ const BUNNY_CDN_IPS = new Set([
|
||||
"84.17.59.115",
|
||||
"89.187.165.194",
|
||||
"138.199.15.193",
|
||||
"89.35.237.170",
|
||||
"37.19.216.130",
|
||||
"185.93.1.247",
|
||||
"185.93.3.244",
|
||||
@@ -150,6 +145,7 @@ const BUNNY_CDN_IPS = new Set([
|
||||
"84.17.63.178",
|
||||
"200.25.32.131",
|
||||
"37.19.207.34",
|
||||
"37.19.207.38",
|
||||
"192.189.65.146",
|
||||
"143.244.45.177",
|
||||
"185.93.1.249",
|
||||
@@ -168,9 +164,7 @@ const BUNNY_CDN_IPS = new Set([
|
||||
"129.227.217.178",
|
||||
"129.227.217.179",
|
||||
"200.25.69.94",
|
||||
"128.1.52.179",
|
||||
"200.25.16.103",
|
||||
"15.235.54.226",
|
||||
"102.67.138.155",
|
||||
"156.146.43.65",
|
||||
"195.181.163.203",
|
||||
@@ -278,13 +272,11 @@ const BUNNY_CDN_IPS = new Set([
|
||||
"107.155.47.146",
|
||||
"193.201.190.174",
|
||||
"156.59.95.218",
|
||||
"213.170.143.139",
|
||||
"129.227.186.154",
|
||||
"195.238.127.98",
|
||||
"200.25.22.6",
|
||||
"204.16.244.92",
|
||||
"200.25.70.101",
|
||||
"200.25.66.100",
|
||||
"139.180.209.182",
|
||||
"103.108.231.41",
|
||||
"103.108.229.5",
|
||||
@@ -387,46 +379,13 @@ const BUNNY_CDN_IPS = new Set([
|
||||
"38.54.5.37",
|
||||
"38.54.3.92",
|
||||
"185.165.170.74",
|
||||
"207.121.80.118",
|
||||
"207.121.46.228",
|
||||
"207.121.46.236",
|
||||
"207.121.46.244",
|
||||
"207.121.46.252",
|
||||
"216.202.235.164",
|
||||
"207.121.46.220",
|
||||
"207.121.75.132",
|
||||
"207.121.80.12",
|
||||
"207.121.80.172",
|
||||
"207.121.90.60",
|
||||
"207.121.90.68",
|
||||
"207.121.97.204",
|
||||
"207.121.90.252",
|
||||
"207.121.97.236",
|
||||
"207.121.99.12",
|
||||
"138.199.24.219",
|
||||
"185.93.2.251",
|
||||
"138.199.46.65",
|
||||
"207.121.41.196",
|
||||
"207.121.99.20",
|
||||
"207.121.99.36",
|
||||
"207.121.99.44",
|
||||
"207.121.99.52",
|
||||
"207.121.99.60",
|
||||
"207.121.23.68",
|
||||
"207.121.23.124",
|
||||
"207.121.23.244",
|
||||
"207.121.23.180",
|
||||
"207.121.23.188",
|
||||
"207.121.23.196",
|
||||
"207.121.23.204",
|
||||
"207.121.24.52",
|
||||
"207.121.24.60",
|
||||
"207.121.24.68",
|
||||
"207.121.24.76",
|
||||
"207.121.24.92",
|
||||
"207.121.24.100",
|
||||
"207.121.24.108",
|
||||
"207.121.24.116",
|
||||
"154.95.86.76",
|
||||
"5.9.99.73",
|
||||
"78.46.92.118",
|
||||
@@ -434,14 +393,52 @@ const BUNNY_CDN_IPS = new Set([
|
||||
"78.46.156.89",
|
||||
"88.198.9.155",
|
||||
"144.76.79.22",
|
||||
"103.1.215.93",
|
||||
"103.137.12.33",
|
||||
"103.107.196.31",
|
||||
"116.90.72.155",
|
||||
"103.137.14.5",
|
||||
"116.90.75.65",
|
||||
"37.19.207.37",
|
||||
"208.83.234.224",
|
||||
"79.127.237.104",
|
||||
"79.127.243.187",
|
||||
"45.156.248.73",
|
||||
"79.127.134.225",
|
||||
"79.127.134.226",
|
||||
"79.127.134.227",
|
||||
"79.127.134.228",
|
||||
"79.127.134.229",
|
||||
"79.127.134.230",
|
||||
"79.127.134.231",
|
||||
"79.127.134.130",
|
||||
"79.127.134.131",
|
||||
"79.127.134.132",
|
||||
"79.127.134.234",
|
||||
"79.127.134.235",
|
||||
"185.111.111.154",
|
||||
"185.111.111.155",
|
||||
"185.111.111.156",
|
||||
"185.111.111.157",
|
||||
"185.111.111.158",
|
||||
"185.111.111.159",
|
||||
"185.111.111.160",
|
||||
"141.227.142.242",
|
||||
"94.128.254.166",
|
||||
"195.206.229.69",
|
||||
"200.25.86.90",
|
||||
"148.113.190.161",
|
||||
"46.151.194.242",
|
||||
"46.151.194.243",
|
||||
"212.102.40.120",
|
||||
"213.170.143.100",
|
||||
"154.93.86.71",
|
||||
"143.244.60.196",
|
||||
"143.244.60.197",
|
||||
"143.244.60.195",
|
||||
"79.127.134.129",
|
||||
"79.127.134.133",
|
||||
"152.233.22.97",
|
||||
"152.233.22.98",
|
||||
"152.233.22.100",
|
||||
"152.233.22.99",
|
||||
"152.233.22.101",
|
||||
"152.233.22.102",
|
||||
"152.233.22.103",
|
||||
"116.202.155.146",
|
||||
"116.202.193.178",
|
||||
"116.202.224.168",
|
||||
@@ -502,6 +499,12 @@ const BUNNY_CDN_IPS = new Set([
|
||||
"103.60.15.166",
|
||||
"103.60.15.167",
|
||||
"103.60.15.168",
|
||||
"176.9.139.94",
|
||||
"148.251.129.132",
|
||||
"148.251.131.73",
|
||||
"148.251.131.74",
|
||||
"136.243.70.170",
|
||||
"148.251.131.238",
|
||||
"109.248.43.116",
|
||||
"109.248.43.117",
|
||||
"109.248.43.162",
|
||||
@@ -527,7 +530,9 @@ const BUNNY_CDN_IPS = new Set([
|
||||
"139.180.129.216",
|
||||
"139.99.174.7",
|
||||
"89.187.169.18",
|
||||
"143.244.38.133",
|
||||
"89.187.179.7",
|
||||
"169.150.213.50",
|
||||
"143.244.62.213",
|
||||
"185.93.3.246",
|
||||
"195.181.163.198",
|
||||
@@ -535,7 +540,6 @@ const BUNNY_CDN_IPS = new Set([
|
||||
"84.17.37.211",
|
||||
"212.102.50.54",
|
||||
"212.102.46.115",
|
||||
"143.244.38.135",
|
||||
"169.150.238.21",
|
||||
"169.150.207.51",
|
||||
"169.150.207.49",
|
||||
@@ -546,7 +550,6 @@ const BUNNY_CDN_IPS = new Set([
|
||||
"169.150.247.139",
|
||||
"169.150.247.177",
|
||||
"169.150.247.178",
|
||||
"169.150.213.49",
|
||||
"212.102.46.119",
|
||||
"84.17.38.234",
|
||||
"84.17.38.233",
|
||||
@@ -558,7 +561,6 @@ const BUNNY_CDN_IPS = new Set([
|
||||
"169.150.247.138",
|
||||
"169.150.247.184",
|
||||
"169.150.247.185",
|
||||
"156.146.58.83",
|
||||
"212.102.43.88",
|
||||
"89.187.169.26",
|
||||
"109.61.89.57",
|
||||
@@ -587,6 +589,17 @@ const BUNNY_CDN_IPS = new Set([
|
||||
"138.199.4.177",
|
||||
"37.19.222.34",
|
||||
"46.151.193.85",
|
||||
"79.127.237.99",
|
||||
"212.104.158.30",
|
||||
"212.104.158.31",
|
||||
"212.104.158.32",
|
||||
"212.104.158.33",
|
||||
"212.104.158.34",
|
||||
"212.104.158.28",
|
||||
"212.104.158.29",
|
||||
"212.104.158.35",
|
||||
"212.104.158.36",
|
||||
"212.104.158.37",
|
||||
"212.104.158.17",
|
||||
"212.104.158.18",
|
||||
"212.104.158.19",
|
||||
@@ -595,12 +608,20 @@ const BUNNY_CDN_IPS = new Set([
|
||||
"212.104.158.22",
|
||||
"212.104.158.24",
|
||||
"212.104.158.26",
|
||||
"79.127.237.134",
|
||||
"89.187.184.177",
|
||||
"89.187.184.179",
|
||||
"89.187.184.173",
|
||||
"89.187.184.178",
|
||||
"89.187.184.176",
|
||||
"212.104.158.25",
|
||||
"212.104.158.27",
|
||||
"212.104.158.67",
|
||||
"212.104.158.10",
|
||||
"212.104.158.12",
|
||||
"212.104.158.64",
|
||||
"212.104.158.16",
|
||||
"212.104.158.23",
|
||||
"212.104.158.54",
|
||||
]);
|
||||
|
||||
// Arvancloud IP ranges
|
||||
|
||||
@@ -18,6 +18,7 @@ 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";
|
||||
@@ -32,6 +33,7 @@ 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,
|
||||
@@ -267,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",
|
||||
@@ -341,6 +359,21 @@ export const rebuildCompose = async ({
|
||||
composeStatus: "done",
|
||||
});
|
||||
} 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",
|
||||
@@ -375,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}`;
|
||||
|
||||
@@ -402,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(
|
||||
@@ -437,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),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ export const createDeployment = async (
|
||||
"application",
|
||||
application.serverId,
|
||||
);
|
||||
const serverId = application.serverId;
|
||||
const serverId = application.buildServerId || application.serverId;
|
||||
|
||||
const { LOGS_PATH } = paths(!!serverId);
|
||||
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
||||
@@ -93,6 +93,7 @@ export const createDeployment = async (
|
||||
const command = `
|
||||
mkdir -p ${LOGS_PATH}/${application.appName};
|
||||
echo "Initializing deployment" >> ${logFilePath};
|
||||
echo "Building on ${serverId ? "Build Server" : "Dokploy Server"}" >> ${logFilePath};
|
||||
`;
|
||||
|
||||
await execAsyncRemote(server.serverId, command);
|
||||
@@ -112,6 +113,9 @@ export const createDeployment = async (
|
||||
logPath: logFilePath,
|
||||
description: deployment.description || "",
|
||||
startedAt: new Date().toISOString(),
|
||||
...(application.buildServerId && {
|
||||
buildServerId: application.buildServerId,
|
||||
}),
|
||||
})
|
||||
.returning();
|
||||
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
|
||||
|
||||
@@ -19,6 +19,7 @@ export const createDomain = async (input: typeof apiCreateDomain._type) => {
|
||||
.insert(domains)
|
||||
.values({
|
||||
...input,
|
||||
host: input.host?.trim(),
|
||||
})
|
||||
.returning()
|
||||
.then((response) => response[0]);
|
||||
@@ -120,6 +121,7 @@ export const updateDomainById = async (
|
||||
.update(domains)
|
||||
.set({
|
||||
...domainData,
|
||||
...(domainData.host && { host: domainData.host.trim() }),
|
||||
})
|
||||
.where(eq(domains.domainId, domainId))
|
||||
.returning();
|
||||
|
||||
@@ -498,7 +498,7 @@ export const createNtfyNotification = async (
|
||||
.values({
|
||||
serverUrl: input.serverUrl,
|
||||
topic: input.topic,
|
||||
accessToken: input.accessToken,
|
||||
accessToken: input.accessToken ?? null,
|
||||
priority: input.priority,
|
||||
})
|
||||
.returning()
|
||||
@@ -569,7 +569,7 @@ export const updateNtfyNotification = async (
|
||||
.set({
|
||||
serverUrl: input.serverUrl,
|
||||
topic: input.topic,
|
||||
accessToken: input.accessToken,
|
||||
accessToken: input.accessToken ?? null,
|
||||
priority: input.priority,
|
||||
})
|
||||
.where(eq(ntfy.ntfyId, input.ntfyId));
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -60,10 +60,7 @@ export const getUpdateData = async (): Promise<IUpdateData> => {
|
||||
try {
|
||||
currentDigest = await getServiceImageDigest();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
// 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
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ export const initializePostgres = async () => {
|
||||
Mounts: [
|
||||
{
|
||||
Type: "volume",
|
||||
Source: "dokploy-postgres-database",
|
||||
Source: "dokploy-postgres",
|
||||
Target: "/var/lib/postgresql/data",
|
||||
},
|
||||
],
|
||||
|
||||
@@ -14,7 +14,7 @@ export const initializeRedis = async () => {
|
||||
Mounts: [
|
||||
{
|
||||
Type: "volume",
|
||||
Source: "redis-data-volume",
|
||||
Source: "dokploy-redis",
|
||||
Target: "/data",
|
||||
},
|
||||
],
|
||||
|
||||
@@ -51,7 +51,12 @@ export const serverSetup = async (
|
||||
});
|
||||
|
||||
try {
|
||||
onData?.("\nInstalling Server Dependencies: ✅\n");
|
||||
const isBuildServer = server.serverType === "build";
|
||||
onData?.(
|
||||
isBuildServer
|
||||
? "\nInstalling Build Server Dependencies: ✅\n"
|
||||
: "\nInstalling Server Dependencies: ✅\n",
|
||||
);
|
||||
await installRequirements(serverId, onData);
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
@@ -65,7 +70,7 @@ export const serverSetup = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const defaultCommand = () => {
|
||||
export const defaultCommand = (isBuildServer = false) => {
|
||||
const bashCommand = `
|
||||
set -e;
|
||||
DOCKER_VERSION=27.0.3
|
||||
@@ -126,6 +131,7 @@ echo -e "---------------------------------------------"
|
||||
echo "| CPU Architecture | $SYS_ARCH"
|
||||
echo "| Operating System | $OS_TYPE $OS_VERSION"
|
||||
echo "| Docker | $DOCKER_VERSION"
|
||||
${isBuildServer ? 'echo "| Server Type | Build Server"' : ""}
|
||||
echo -e "---------------------------------------------\n"
|
||||
echo -e "1. Installing required packages (curl, wget, git, jq, openssl). "
|
||||
|
||||
@@ -135,6 +141,9 @@ command_exists() {
|
||||
|
||||
${installUtilities()}
|
||||
|
||||
${
|
||||
!isBuildServer
|
||||
? `
|
||||
echo -e "2. Validating ports. "
|
||||
${validatePorts()}
|
||||
|
||||
@@ -173,6 +182,25 @@ ${installBuildpacks()}
|
||||
|
||||
echo -e "13. Installing Railpack"
|
||||
${installRailpack()}
|
||||
`
|
||||
: `
|
||||
echo -e "2. Installing Docker. "
|
||||
${installDocker()}
|
||||
|
||||
echo -e "3. Setting up Directories"
|
||||
${setupMainDirectory()}
|
||||
${setupDirectories()}
|
||||
|
||||
echo -e "4. Installing Nixpacks"
|
||||
${installNixpacks()}
|
||||
|
||||
echo -e "5. Installing Buildpacks"
|
||||
${installBuildpacks()}
|
||||
|
||||
echo -e "6. Installing Railpack"
|
||||
${installRailpack()}
|
||||
`
|
||||
}
|
||||
`;
|
||||
|
||||
return bashCommand;
|
||||
@@ -189,10 +217,12 @@ const installRequirements = async (
|
||||
throw new Error("No SSH Key found");
|
||||
}
|
||||
|
||||
const isBuildServer = server.serverType === "build";
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
client
|
||||
.once("ready", () => {
|
||||
const command = server.command || defaultCommand();
|
||||
const command = server.command || defaultCommand(isBuildServer);
|
||||
client.exec(command, (err, stream) => {
|
||||
if (err) {
|
||||
onData?.(err.message);
|
||||
|
||||
@@ -2,6 +2,7 @@ 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 { quote } from "shell-quote";
|
||||
import { writeDomainsToCompose } from "../docker/domain";
|
||||
import {
|
||||
encodeBase64,
|
||||
@@ -52,9 +53,8 @@ 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(" ")} 2>&1 || { echo "Error: ❌ Docker command failed"; exit 1; }
|
||||
env -i PATH="$PATH" ${exportEnvCommand} docker ${command.split(" ").join(" ")} 2>&1 || { echo "Error: ❌ Docker command failed"; exit 1; }
|
||||
${compose.isolatedDeployment ? `docker network connect ${compose.appName} $(docker ps --filter "name=dokploy-traefik" -q) >/dev/null 2>&1` : ""}
|
||||
|
||||
echo "Docker Compose Deployed: ✅";
|
||||
@@ -65,7 +65,6 @@ Compose Type: ${composeType} ✅`;
|
||||
`;
|
||||
|
||||
return bashCommand;
|
||||
// return await execAsyncRemote(compose.serverId, bashCommand);
|
||||
};
|
||||
|
||||
const sanitizeCommand = (command: string) => {
|
||||
@@ -137,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}` : "";
|
||||
};
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import {
|
||||
getEnviromentVariablesObject,
|
||||
prepareEnvironmentVariables,
|
||||
prepareEnvironmentVariablesForShell,
|
||||
} from "@dokploy/server/utils/docker/utils";
|
||||
import { quote } from "shell-quote";
|
||||
import {
|
||||
getBuildAppDirectory,
|
||||
getDockerContextPath,
|
||||
@@ -40,14 +41,14 @@ export const getDockerCommand = (application: ApplicationNested) => {
|
||||
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(
|
||||
@@ -57,7 +58,7 @@ export const getDockerCommand = (application: ApplicationNested) => {
|
||||
);
|
||||
|
||||
const joinedSecrets = Object.entries(secrets)
|
||||
.map(([key, value]) => `${key}='${value.replace(/'/g, "'\"'\"'")}'`)
|
||||
.map(([key, value]) => `${key}=${quote([value])}`)
|
||||
.join(" ");
|
||||
|
||||
for (const key in secrets) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { prepareEnvironmentVariables } from "../docker/utils";
|
||||
import { prepareEnvironmentVariablesForShell } from "../docker/utils";
|
||||
import { getBuildAppDirectory } from "../filesystem/directory";
|
||||
import type { ApplicationNested } from ".";
|
||||
|
||||
@@ -6,7 +6,7 @@ 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,
|
||||
@@ -26,7 +26,7 @@ export const getHerokuCommand = (application: ApplicationNested) => {
|
||||
}
|
||||
|
||||
for (const env of envVariables) {
|
||||
args.push("--env", `'${env}'`);
|
||||
args.push("--env", env);
|
||||
}
|
||||
|
||||
const command = `pack ${args.join(" ")}`;
|
||||
|
||||
@@ -29,13 +29,14 @@ export type ApplicationNested = InferResultType<
|
||||
redirects: true;
|
||||
ports: true;
|
||||
registry: true;
|
||||
buildRegistry: true;
|
||||
environment: { with: { project: true } };
|
||||
}
|
||||
>;
|
||||
|
||||
export const getBuildCommand = (application: ApplicationNested) => {
|
||||
let command = "";
|
||||
const { buildType, registry } = application;
|
||||
const { buildType } = application;
|
||||
|
||||
if (application.sourceType === "docker") {
|
||||
return "";
|
||||
@@ -60,7 +61,7 @@ export const getBuildCommand = (application: ApplicationNested) => {
|
||||
command = getRailpackCommand(application);
|
||||
break;
|
||||
}
|
||||
if (registry) {
|
||||
if (application.registry || application.buildRegistry) {
|
||||
command += uploadImageRemoteCommand(application);
|
||||
}
|
||||
|
||||
@@ -79,6 +80,7 @@ export const mechanizeDockerContainer = async (
|
||||
memoryReservation,
|
||||
cpuReservation,
|
||||
command,
|
||||
args,
|
||||
ports,
|
||||
} = application;
|
||||
|
||||
@@ -125,12 +127,16 @@ export const mechanizeDockerContainer = async (
|
||||
Image: image,
|
||||
Env: envVariables,
|
||||
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
|
||||
...(command
|
||||
? {
|
||||
Command: ["/bin/sh"],
|
||||
Args: ["-c", command],
|
||||
}
|
||||
: {}),
|
||||
...(StopGracePeriod !== null &&
|
||||
StopGracePeriod !== undefined && { StopGracePeriod }),
|
||||
...(command && {
|
||||
Command: command.split(" "),
|
||||
}),
|
||||
...(args &&
|
||||
args.length > 0 && {
|
||||
Args: args,
|
||||
}),
|
||||
|
||||
Labels,
|
||||
},
|
||||
Networks,
|
||||
@@ -153,8 +159,6 @@ export const mechanizeDockerContainer = async (
|
||||
})),
|
||||
},
|
||||
UpdateConfig,
|
||||
...(StopGracePeriod !== undefined &&
|
||||
StopGracePeriod !== null && { StopGracePeriod }),
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -169,13 +173,15 @@ export const mechanizeDockerContainer = async (
|
||||
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
await docker.createService(settings);
|
||||
}
|
||||
};
|
||||
|
||||
const getImageName = (application: ApplicationNested) => {
|
||||
const { appName, sourceType, dockerImage, registry } = application;
|
||||
const { appName, sourceType, dockerImage, registry, buildRegistry } =
|
||||
application;
|
||||
const imageName = `${appName}:latest`;
|
||||
if (sourceType === "docker") {
|
||||
return dockerImage || "ERROR-NO-IMAGE-PROVIDED";
|
||||
@@ -188,12 +194,26 @@ const getImageName = (application: ApplicationNested) => {
|
||||
: `${registryUrl ? `${registryUrl}/` : ""}${username}/${imageName}`;
|
||||
return registryTag;
|
||||
}
|
||||
if (buildRegistry) {
|
||||
const { registryUrl, imagePrefix, username } = buildRegistry;
|
||||
const registryTag = imagePrefix
|
||||
? `${registryUrl ? `${registryUrl}/` : ""}${imagePrefix}/${imageName}`
|
||||
: `${registryUrl ? `${registryUrl}/` : ""}${username}/${imageName}`;
|
||||
return registryTag;
|
||||
}
|
||||
|
||||
return imageName;
|
||||
};
|
||||
|
||||
export const getAuthConfig = (application: ApplicationNested) => {
|
||||
const { registry, username, password, sourceType, registryUrl } = application;
|
||||
const {
|
||||
registry,
|
||||
buildRegistry,
|
||||
username,
|
||||
password,
|
||||
sourceType,
|
||||
registryUrl,
|
||||
} = application;
|
||||
|
||||
if (sourceType === "docker") {
|
||||
if (username && password) {
|
||||
@@ -209,6 +229,12 @@ export const getAuthConfig = (application: ApplicationNested) => {
|
||||
username: registry.username,
|
||||
serveraddress: registry.registryUrl,
|
||||
};
|
||||
} else if (buildRegistry) {
|
||||
return {
|
||||
password: buildRegistry.password,
|
||||
username: buildRegistry.username,
|
||||
serveraddress: buildRegistry.registryUrl,
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import path from "node:path";
|
||||
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 type { ApplicationNested } from ".";
|
||||
|
||||
@@ -10,7 +10,7 @@ export const getNixpacksCommand = (application: ApplicationNested) => {
|
||||
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
const buildContainerId = `${appName}-${nanoid(10)}`;
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
const envVariables = prepareEnvironmentVariablesForShell(
|
||||
env,
|
||||
application.environment.project.env,
|
||||
application.environment.env,
|
||||
@@ -23,7 +23,7 @@ export const getNixpacksCommand = (application: ApplicationNested) => {
|
||||
}
|
||||
|
||||
for (const env of envVariables) {
|
||||
args.push("--env", `'${env}'`);
|
||||
args.push("--env", env);
|
||||
}
|
||||
|
||||
if (publishDirectory) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { prepareEnvironmentVariables } from "../docker/utils";
|
||||
import { prepareEnvironmentVariablesForShell } from "../docker/utils";
|
||||
import { getBuildAppDirectory } from "../filesystem/directory";
|
||||
import type { ApplicationNested } from ".";
|
||||
|
||||
@@ -6,7 +6,7 @@ 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,
|
||||
@@ -26,7 +26,7 @@ export const getPaketoCommand = (application: ApplicationNested) => {
|
||||
}
|
||||
|
||||
for (const env of envVariables) {
|
||||
args.push("--env", `'${env}'`);
|
||||
args.push("--env", env);
|
||||
}
|
||||
|
||||
const command = `pack ${args.join(" ")}`;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { nanoid } from "nanoid";
|
||||
import { quote } from "shell-quote";
|
||||
import {
|
||||
parseEnvironmentKeyValuePair,
|
||||
prepareEnvironmentVariables,
|
||||
prepareEnvironmentVariablesForShell,
|
||||
} from "../docker/utils";
|
||||
import { getBuildAppDirectory } from "../filesystem/directory";
|
||||
import type { ApplicationNested } from ".";
|
||||
@@ -18,7 +20,7 @@ const calculateSecretsHash = (envVariables: string[]): string => {
|
||||
export const getRailpackCommand = (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,
|
||||
@@ -35,7 +37,7 @@ export const getRailpackCommand = (application: ApplicationNested) => {
|
||||
];
|
||||
|
||||
for (const env of envVariables) {
|
||||
prepareArgs.push("--env", `'${env}'`);
|
||||
prepareArgs.push("--env", env);
|
||||
}
|
||||
|
||||
// Calculate secrets hash for layer invalidation
|
||||
@@ -63,12 +65,18 @@ export const getRailpackCommand = (application: ApplicationNested) => {
|
||||
];
|
||||
|
||||
// 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])}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +85,9 @@ export const getRailpackCommand = (application: ApplicationNested) => {
|
||||
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
|
||||
|
||||
|
||||
@@ -1,44 +1,77 @@
|
||||
import type { Registry } from "@dokploy/server/services/registry";
|
||||
import type { ApplicationNested } from "../builders";
|
||||
|
||||
export const uploadImageRemoteCommand = (application: ApplicationNested) => {
|
||||
const registry = application.registry;
|
||||
const buildRegistry = application.buildRegistry;
|
||||
|
||||
if (!registry) {
|
||||
throw new Error("Registry not found");
|
||||
if (!registry && !buildRegistry) {
|
||||
throw new Error("No registry 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 ? `${registryUrl}/` : ""}${imagePrefix}/${imageName}`
|
||||
: `${registryUrl ? `${registryUrl}/` : ""}${username}/${imageName}`;
|
||||
|
||||
const commands: string[] = [];
|
||||
if (registry) {
|
||||
const registryTag = getRegistryTag(registry, imageName);
|
||||
if (registryTag) {
|
||||
commands.push(`echo "📦 [Enabled Registry Swarm]"`);
|
||||
commands.push(getRegistryCommands(registry, imageName, registryTag));
|
||||
}
|
||||
}
|
||||
if (buildRegistry) {
|
||||
const buildRegistryTag = getRegistryTag(buildRegistry, imageName);
|
||||
if (buildRegistryTag) {
|
||||
commands.push(`echo "🔑 [Enabled Build Registry]"`);
|
||||
commands.push(
|
||||
getRegistryCommands(buildRegistry, imageName, buildRegistryTag),
|
||||
);
|
||||
commands.push(
|
||||
`echo "⚠️ INFO: After the build is finished, you need to wait a few seconds for the server to download the image and run the container."`,
|
||||
);
|
||||
commands.push(
|
||||
`echo "📊 Check the Logs tab to see when the container starts running."`,
|
||||
);
|
||||
}
|
||||
}
|
||||
try {
|
||||
const command = `
|
||||
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" ;
|
||||
docker tag ${imageName} ${registryTag} || {
|
||||
echo "❌ Error tagging image" ;
|
||||
exit 1;
|
||||
}
|
||||
echo "✅ Image Tagged" ;
|
||||
docker push ${registryTag} || {
|
||||
echo "❌ Error pushing image" ;
|
||||
exit 1;
|
||||
}
|
||||
echo "✅ Image Pushed" ;
|
||||
`;
|
||||
return command;
|
||||
return commands.join("\n");
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
const getRegistryTag = (registry: Registry | null, imageName: string) => {
|
||||
if (!registry) {
|
||||
return null;
|
||||
}
|
||||
const { registryUrl, imagePrefix, username } = registry;
|
||||
return imagePrefix
|
||||
? `${registryUrl ? `${registryUrl}/` : ""}${imagePrefix}/${imageName}`
|
||||
: `${registryUrl ? `${registryUrl}/` : ""}${username}/${imageName}`;
|
||||
};
|
||||
|
||||
const getRegistryCommands = (
|
||||
registry: Registry,
|
||||
imageName: string,
|
||||
registryTag: string,
|
||||
): string => {
|
||||
return `
|
||||
echo "📦 [Enabled Registry] Uploading image to '${registry.registryType}' | '${registryTag}'" ;
|
||||
echo "${registry.password}" | docker login ${registry.registryUrl} -u ${registry.username} --password-stdin || {
|
||||
echo "❌ DockerHub Failed" ;
|
||||
exit 1;
|
||||
}
|
||||
echo "✅ Registry Login Success" ;
|
||||
docker tag ${imageName} ${registryTag} || {
|
||||
echo "❌ Error tagging image" ;
|
||||
exit 1;
|
||||
}
|
||||
echo "✅ Image Tagged" ;
|
||||
docker push ${registryTag} || {
|
||||
echo "❌ Error pushing image" ;
|
||||
exit 1;
|
||||
}
|
||||
echo "✅ Image Pushed" ;
|
||||
`;
|
||||
};
|
||||
|
||||
@@ -29,6 +29,7 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
|
||||
cpuLimit,
|
||||
cpuReservation,
|
||||
command,
|
||||
args,
|
||||
mounts,
|
||||
} = mariadb;
|
||||
|
||||
@@ -73,12 +74,16 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
|
||||
Image: dockerImage,
|
||||
Env: envVariables,
|
||||
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
|
||||
...(command
|
||||
? {
|
||||
Command: ["/bin/sh"],
|
||||
Args: ["-c", command],
|
||||
}
|
||||
: {}),
|
||||
...(StopGracePeriod !== null &&
|
||||
StopGracePeriod !== undefined && { StopGracePeriod }),
|
||||
...(command && {
|
||||
Command: command.split(" "),
|
||||
}),
|
||||
...(args &&
|
||||
args.length > 0 && {
|
||||
Args: args,
|
||||
}),
|
||||
|
||||
Labels,
|
||||
},
|
||||
Networks,
|
||||
@@ -106,8 +111,6 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
|
||||
: [],
|
||||
},
|
||||
UpdateConfig,
|
||||
...(StopGracePeriod !== undefined &&
|
||||
StopGracePeriod !== null && { StopGracePeriod }),
|
||||
};
|
||||
try {
|
||||
const service = docker.getService(appName);
|
||||
|
||||
@@ -28,6 +28,7 @@ export const buildMongo = async (mongo: MongoNested) => {
|
||||
databaseUser,
|
||||
databasePassword,
|
||||
command,
|
||||
args,
|
||||
mounts,
|
||||
replicaSets,
|
||||
} = mongo;
|
||||
@@ -121,17 +122,24 @@ ${command ?? "wait $MONGOD_PID"}`;
|
||||
Image: dockerImage,
|
||||
Env: envVariables,
|
||||
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
|
||||
...(StopGracePeriod !== null &&
|
||||
StopGracePeriod !== undefined && { StopGracePeriod }),
|
||||
...(replicaSets
|
||||
? {
|
||||
Command: ["/bin/bash"],
|
||||
Args: ["-c", startupScript],
|
||||
}
|
||||
: {
|
||||
...(command && {
|
||||
Command: ["/bin/bash"],
|
||||
Args: ["-c", command],
|
||||
}),
|
||||
}),
|
||||
: {}),
|
||||
...(command &&
|
||||
!replicaSets && {
|
||||
Command: command.split(" "),
|
||||
}),
|
||||
...(args &&
|
||||
args.length > 0 &&
|
||||
!replicaSets && {
|
||||
Args: args,
|
||||
}),
|
||||
|
||||
Labels,
|
||||
},
|
||||
Networks,
|
||||
@@ -159,8 +167,6 @@ ${command ?? "wait $MONGOD_PID"}`;
|
||||
: [],
|
||||
},
|
||||
UpdateConfig,
|
||||
...(StopGracePeriod !== undefined &&
|
||||
StopGracePeriod !== null && { StopGracePeriod }),
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
@@ -30,6 +30,7 @@ export const buildMysql = async (mysql: MysqlNested) => {
|
||||
cpuLimit,
|
||||
cpuReservation,
|
||||
command,
|
||||
args,
|
||||
mounts,
|
||||
} = mysql;
|
||||
|
||||
@@ -79,12 +80,16 @@ export const buildMysql = async (mysql: MysqlNested) => {
|
||||
Image: dockerImage,
|
||||
Env: envVariables,
|
||||
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
|
||||
...(command
|
||||
? {
|
||||
Command: ["/bin/sh"],
|
||||
Args: ["-c", command],
|
||||
}
|
||||
: {}),
|
||||
...(StopGracePeriod !== null &&
|
||||
StopGracePeriod !== undefined && { StopGracePeriod }),
|
||||
...(command && {
|
||||
Command: command.split(" "),
|
||||
}),
|
||||
...(args &&
|
||||
args.length > 0 && {
|
||||
Args: args,
|
||||
}),
|
||||
|
||||
Labels,
|
||||
},
|
||||
Networks,
|
||||
@@ -112,8 +117,6 @@ export const buildMysql = async (mysql: MysqlNested) => {
|
||||
: [],
|
||||
},
|
||||
UpdateConfig,
|
||||
...(StopGracePeriod !== undefined &&
|
||||
StopGracePeriod !== null && { StopGracePeriod }),
|
||||
};
|
||||
try {
|
||||
const service = docker.getService(appName);
|
||||
|
||||
@@ -28,6 +28,7 @@ export const buildPostgres = async (postgres: PostgresNested) => {
|
||||
databaseUser,
|
||||
databasePassword,
|
||||
command,
|
||||
args,
|
||||
mounts,
|
||||
} = postgres;
|
||||
|
||||
@@ -72,12 +73,16 @@ export const buildPostgres = async (postgres: PostgresNested) => {
|
||||
Image: dockerImage,
|
||||
Env: envVariables,
|
||||
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
|
||||
...(command
|
||||
? {
|
||||
Command: ["/bin/sh"],
|
||||
Args: ["-c", command],
|
||||
}
|
||||
: {}),
|
||||
...(StopGracePeriod !== null &&
|
||||
StopGracePeriod !== undefined && { StopGracePeriod }),
|
||||
...(command && {
|
||||
Command: command.split(" "),
|
||||
}),
|
||||
...(args &&
|
||||
args.length > 0 && {
|
||||
Args: args,
|
||||
}),
|
||||
|
||||
Labels,
|
||||
},
|
||||
Networks,
|
||||
@@ -105,8 +110,6 @@ export const buildPostgres = async (postgres: PostgresNested) => {
|
||||
: [],
|
||||
},
|
||||
UpdateConfig,
|
||||
...(StopGracePeriod !== undefined &&
|
||||
StopGracePeriod !== null && { StopGracePeriod }),
|
||||
};
|
||||
try {
|
||||
const service = docker.getService(appName);
|
||||
|
||||
@@ -26,6 +26,7 @@ export const buildRedis = async (redis: RedisNested) => {
|
||||
cpuLimit,
|
||||
cpuReservation,
|
||||
command,
|
||||
args,
|
||||
mounts,
|
||||
} = redis;
|
||||
|
||||
@@ -70,11 +71,22 @@ export const buildRedis = async (redis: RedisNested) => {
|
||||
Image: dockerImage,
|
||||
Env: envVariables,
|
||||
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
|
||||
Command: ["/bin/sh"],
|
||||
Args: [
|
||||
"-c",
|
||||
command ? command : `redis-server --requirepass ${databasePassword}`,
|
||||
],
|
||||
...(StopGracePeriod !== null &&
|
||||
StopGracePeriod !== undefined && { StopGracePeriod }),
|
||||
...(command || args
|
||||
? {
|
||||
...(command && {
|
||||
Command: command.split(" "),
|
||||
}),
|
||||
...(args &&
|
||||
args.length > 0 && {
|
||||
Args: args,
|
||||
}),
|
||||
}
|
||||
: {
|
||||
Command: ["/bin/sh"],
|
||||
Args: ["-c", `redis-server --requirepass ${databasePassword}`],
|
||||
}),
|
||||
Labels,
|
||||
},
|
||||
Networks,
|
||||
@@ -102,8 +114,6 @@ export const buildRedis = async (redis: RedisNested) => {
|
||||
: [],
|
||||
},
|
||||
UpdateConfig,
|
||||
...(StopGracePeriod !== undefined &&
|
||||
StopGracePeriod !== null && { StopGracePeriod }),
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
@@ -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] => {
|
||||
|
||||
@@ -7,8 +7,8 @@ import { and, eq } from "drizzle-orm";
|
||||
import {
|
||||
sendDiscordNotification,
|
||||
sendEmailNotification,
|
||||
sendLarkNotification,
|
||||
sendGotifyNotification,
|
||||
sendLarkNotification,
|
||||
sendNtfyNotification,
|
||||
sendSlackNotification,
|
||||
sendTelegramNotification,
|
||||
@@ -52,279 +52,287 @@ export const sendBuildErrorNotifications = async ({
|
||||
for (const notification of notificationList) {
|
||||
const { email, discord, telegram, slack, gotify, ntfy, lark } =
|
||||
notification;
|
||||
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);
|
||||
}
|
||||
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();
|
||||
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,
|
||||
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: `<t:${unixDate}:D>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`⌚`", "Time"),
|
||||
value: `<t:${unixDate}:t>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`❓`", "Type"),
|
||||
value: "Failed",
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`⚠️`", "Error Message"),
|
||||
value: `\`\`\`${truncatedErrorMessage}\`\`\``,
|
||||
},
|
||||
{
|
||||
name: decorate("`🧷`", "Build Link"),
|
||||
value: `[Click here to access build link](${buildLink})`,
|
||||
},
|
||||
],
|
||||
timestamp: date.toISOString(),
|
||||
footer: {
|
||||
text: "Dokploy Build Notification",
|
||||
},
|
||||
{
|
||||
name: decorate("`⚙️`", "Application"),
|
||||
value: applicationName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`❔`", "Type"),
|
||||
value: applicationType,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`📅`", "Date"),
|
||||
value: `<t:${unixDate}:D>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`⌚`", "Time"),
|
||||
value: `<t:${unixDate}:t>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`❓`", "Type"),
|
||||
value: "Failed",
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`⚠️`", "Error Message"),
|
||||
value: `\`\`\`${truncatedErrorMessage}\`\`\``,
|
||||
},
|
||||
{
|
||||
name: decorate("`🧷`", "Build Link"),
|
||||
value: `[Click here to access build link](${buildLink})`,
|
||||
},
|
||||
],
|
||||
timestamp: date.toISOString(),
|
||||
footer: {
|
||||
text: "Dokploy Build Notification",
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (gotify) {
|
||||
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 (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 (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,
|
||||
},
|
||||
],
|
||||
];
|
||||
if (telegram) {
|
||||
const inlineButton = [
|
||||
[
|
||||
{
|
||||
text: "Deployment Logs",
|
||||
url: buildLink,
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
await sendTelegramNotification(
|
||||
telegram,
|
||||
`<b>⚠️ Build Failed</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Type:</b> ${applicationType}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}\n\n<b>Error:</b>\n<pre>${errorMessage}</pre>`,
|
||||
inlineButton,
|
||||
);
|
||||
}
|
||||
await sendTelegramNotification(
|
||||
telegram,
|
||||
`<b>⚠️ Build Failed</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Type:</b> ${applicationType}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}\n\n<b>Error:</b>\n<pre>${errorMessage}</pre>`,
|
||||
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 (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 (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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8,8 +8,8 @@ import { and, eq } from "drizzle-orm";
|
||||
import {
|
||||
sendDiscordNotification,
|
||||
sendEmailNotification,
|
||||
sendLarkNotification,
|
||||
sendGotifyNotification,
|
||||
sendLarkNotification,
|
||||
sendNtfyNotification,
|
||||
sendSlackNotification,
|
||||
sendTelegramNotification,
|
||||
@@ -22,6 +22,7 @@ interface Props {
|
||||
buildLink: string;
|
||||
organizationId: string;
|
||||
domains: Domain[];
|
||||
environmentName: string;
|
||||
}
|
||||
|
||||
export const sendBuildSuccessNotifications = async ({
|
||||
@@ -31,6 +32,7 @@ export const sendBuildSuccessNotifications = async ({
|
||||
buildLink,
|
||||
organizationId,
|
||||
domains,
|
||||
environmentName,
|
||||
}: Props) => {
|
||||
const date = new Date();
|
||||
const unixDate = ~~(Number(date) / 1000);
|
||||
@@ -53,269 +55,295 @@ export const sendBuildSuccessNotifications = async ({
|
||||
for (const notification of notificationList) {
|
||||
const { email, discord, telegram, slack, gotify, ntfy, lark } =
|
||||
notification;
|
||||
|
||||
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();
|
||||
|
||||
await sendDiscordNotification(discord, {
|
||||
title: decorate(">", "`✅` Build Success"),
|
||||
color: 0x57f287,
|
||||
fields: [
|
||||
{
|
||||
name: decorate("`🛠️`", "Project"),
|
||||
value: projectName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`⚙️`", "Application"),
|
||||
value: applicationName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`❔`", "Type"),
|
||||
value: applicationType,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`📅`", "Date"),
|
||||
value: `<t:${unixDate}:D>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`⌚`", "Time"),
|
||||
value: `<t:${unixDate}:t>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`❓`", "Type"),
|
||||
value: "Successful",
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`🧷`", "Build Link"),
|
||||
value: `[Click here to access build link](${buildLink})`,
|
||||
},
|
||||
],
|
||||
timestamp: date.toISOString(),
|
||||
footer: {
|
||||
text: "Dokploy Build Notification",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (gotify) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${gotify.decoration ? decoration : ""} ${text}\n`;
|
||||
await sendGotifyNotification(
|
||||
gotify,
|
||||
decorate("✅", "Build Success"),
|
||||
`${decorate("🛠️", `Project: ${projectName}`)}` +
|
||||
`${decorate("⚙️", `Application: ${applicationName}`)}` +
|
||||
`${decorate("❔", `Type: ${applicationType}`)}` +
|
||||
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
|
||||
`${decorate("🔗", `Build details:\n${buildLink}`)}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (ntfy) {
|
||||
await sendNtfyNotification(
|
||||
ntfy,
|
||||
"Build Success",
|
||||
"white_check_mark",
|
||||
`view, Build details, ${buildLink}, clear=true;`,
|
||||
`🛠Project: ${projectName}\n` +
|
||||
`⚙️Application: ${applicationName}\n` +
|
||||
`❔Type: ${applicationType}\n` +
|
||||
`🕒Date: ${date.toLocaleString()}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (telegram) {
|
||||
const chunkArray = <T>(array: T[], chunkSize: number): T[][] =>
|
||||
Array.from({ length: Math.ceil(array.length / chunkSize) }, (_, i) =>
|
||||
array.slice(i * chunkSize, i * chunkSize + chunkSize),
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
const inlineButton = [
|
||||
[
|
||||
{
|
||||
text: "Deployment Logs",
|
||||
url: buildLink,
|
||||
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: `<t:${unixDate}:D>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`⌚`", "Time"),
|
||||
value: `<t:${unixDate}:t>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`❓`", "Type"),
|
||||
value: "Successful",
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`🧷`", "Build Link"),
|
||||
value: `[Click here to access build link](${buildLink})`,
|
||||
},
|
||||
],
|
||||
timestamp: date.toISOString(),
|
||||
footer: {
|
||||
text: "Dokploy Build Notification",
|
||||
},
|
||||
],
|
||||
...chunkArray(domains, 2).map((chunk) =>
|
||||
chunk.map((data) => ({
|
||||
text: data.host,
|
||||
url: `${data.https ? "https" : "http"}://${data.host}`,
|
||||
})),
|
||||
),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
await sendTelegramNotification(
|
||||
telegram,
|
||||
`<b>✅ Build Success</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Type:</b> ${applicationType}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`,
|
||||
inlineButton,
|
||||
);
|
||||
}
|
||||
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 (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 (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 (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 (telegram) {
|
||||
const chunkArray = <T>(array: T[], chunkSize: number): T[][] =>
|
||||
Array.from({ length: Math.ceil(array.length / chunkSize) }, (_, i) =>
|
||||
array.slice(i * chunkSize, i * chunkSize + chunkSize),
|
||||
);
|
||||
|
||||
const inlineButton = [
|
||||
[
|
||||
{
|
||||
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,
|
||||
`<b>✅ Build Success</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Environment:</b> ${environmentName}\n<b>Type:</b> ${applicationType}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`,
|
||||
inlineButton,
|
||||
);
|
||||
}
|
||||
|
||||
if (slack) {
|
||||
const { channel } = slack;
|
||||
await sendSlackNotification(slack, {
|
||||
channel: channel,
|
||||
attachments: [
|
||||
{
|
||||
color: "#00FF00",
|
||||
pretext: ":white_check_mark: *Build Success*",
|
||||
fields: [
|
||||
{
|
||||
title: "Project",
|
||||
value: projectName,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Application",
|
||||
value: applicationName,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Environment",
|
||||
value: environmentName,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Type",
|
||||
value: applicationType,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Time",
|
||||
value: date.toLocaleString(),
|
||||
short: true,
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
type: "button",
|
||||
text: "View Build Details",
|
||||
url: buildLink,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (lark) {
|
||||
await sendLarkNotification(lark, {
|
||||
msg_type: "interactive",
|
||||
card: {
|
||||
schema: "2.0",
|
||||
config: {
|
||||
update_multi: true,
|
||||
style: {
|
||||
text_size: {
|
||||
normal_v2: {
|
||||
default: "normal",
|
||||
pc: "normal",
|
||||
mobile: "heading",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
header: {
|
||||
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,
|
||||
},
|
||||
],
|
||||
header: {
|
||||
title: {
|
||||
tag: "plain_text",
|
||||
content: "✅ Build Success",
|
||||
},
|
||||
{
|
||||
tag: "button",
|
||||
text: {
|
||||
tag: "plain_text",
|
||||
content: "View Build Details",
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
type: "primary",
|
||||
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: "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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -7,8 +7,8 @@ import { and, eq } from "drizzle-orm";
|
||||
import {
|
||||
sendDiscordNotification,
|
||||
sendEmailNotification,
|
||||
sendLarkNotification,
|
||||
sendGotifyNotification,
|
||||
sendLarkNotification,
|
||||
sendNtfyNotification,
|
||||
sendSlackNotification,
|
||||
sendTelegramNotification,
|
||||
@@ -52,309 +52,312 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
for (const notification of notificationList) {
|
||||
const { email, discord, telegram, slack, gotify, ntfy, 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({
|
||||
projectName,
|
||||
applicationName,
|
||||
databaseType,
|
||||
type,
|
||||
errorMessage,
|
||||
date: date.toLocaleString(),
|
||||
}),
|
||||
).catch();
|
||||
await sendEmailNotification(
|
||||
email,
|
||||
"Database backup 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:
|
||||
type === "success"
|
||||
? decorate(">", "`✅` Database Backup Successful")
|
||||
: decorate(">", "`❌` Database Backup Failed"),
|
||||
color: type === "success" ? 0x57f287 : 0xed4245,
|
||||
fields: [
|
||||
{
|
||||
name: decorate("`🛠️`", "Project"),
|
||||
value: projectName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`⚙️`", "Application"),
|
||||
value: applicationName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`❔`", "Database"),
|
||||
value: databaseType,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`📂`", "Database Name"),
|
||||
value: databaseName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`📅`", "Date"),
|
||||
value: `<t:${unixDate}:D>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`⌚`", "Time"),
|
||||
value: `<t:${unixDate}:t>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`❓`", "Type"),
|
||||
value: type
|
||||
.replace("error", "Failed")
|
||||
.replace("success", "Successful"),
|
||||
inline: true,
|
||||
},
|
||||
...(type === "error" && errorMessage
|
||||
? [
|
||||
{
|
||||
name: decorate("`⚠️`", "Error Message"),
|
||||
value: `\`\`\`${errorMessage}\`\`\``,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
timestamp: date.toISOString(),
|
||||
footer: {
|
||||
text: "Dokploy Database Backup Notification",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await sendDiscordNotification(discord, {
|
||||
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: `<t:${unixDate}:D>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`⌚`", "Time"),
|
||||
value: `<t:${unixDate}:t>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`❓`", "Type"),
|
||||
value: type
|
||||
.replace("error", "Failed")
|
||||
.replace("success", "Successful"),
|
||||
inline: true,
|
||||
},
|
||||
...(type === "error" && errorMessage
|
||||
? [
|
||||
{
|
||||
name: decorate("`⚠️`", "Error Message"),
|
||||
value: `\`\`\`${errorMessage}\`\`\``,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
timestamp: date.toISOString(),
|
||||
footer: {
|
||||
text: "Dokploy Database Backup Notification",
|
||||
},
|
||||
});
|
||||
}
|
||||
if (gotify) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${gotify.decoration ? decoration : ""} ${text}\n`;
|
||||
|
||||
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}`) : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
await sendGotifyNotification(
|
||||
gotify,
|
||||
decorate(
|
||||
type === "success" ? "✅" : "❌",
|
||||
if (ntfy) {
|
||||
await sendNtfyNotification(
|
||||
ntfy,
|
||||
`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}`) : ""}`,
|
||||
);
|
||||
}
|
||||
`${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 (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;
|
||||
|
||||
if (telegram) {
|
||||
const isError = type === "error" && errorMessage;
|
||||
const statusEmoji = type === "success" ? "✅" : "❌";
|
||||
const typeStatus = type === "success" ? "Successful" : "Failed";
|
||||
const errorMsg = isError
|
||||
? `\n\n<b>Error:</b>\n<pre>${errorMessage}</pre>`
|
||||
: "";
|
||||
|
||||
const statusEmoji = type === "success" ? "✅" : "❌";
|
||||
const typeStatus = type === "success" ? "Successful" : "Failed";
|
||||
const errorMsg = isError
|
||||
? `\n\n<b>Error:</b>\n<pre>${errorMessage}</pre>`
|
||||
: "";
|
||||
const messageText = `<b>${statusEmoji} Database Backup ${typeStatus}</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Type:</b> ${databaseType}\n<b>Database Name:</b> ${databaseName}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}${isError ? errorMsg : ""}`;
|
||||
|
||||
const messageText = `<b>${statusEmoji} Database Backup ${typeStatus}</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Type:</b> ${databaseType}\n<b>Database Name:</b> ${databaseName}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}${isError ? errorMsg : ""}`;
|
||||
await sendTelegramNotification(telegram, messageText);
|
||||
}
|
||||
|
||||
await sendTelegramNotification(telegram, messageText);
|
||||
}
|
||||
if (slack) {
|
||||
const { channel } = slack;
|
||||
await sendSlackNotification(slack, {
|
||||
channel: channel,
|
||||
attachments: [
|
||||
{
|
||||
color: type === "success" ? "#00FF00" : "#FF0000",
|
||||
pretext:
|
||||
type === "success"
|
||||
? ":white_check_mark: *Database Backup Successful*"
|
||||
: ":x: *Database Backup Failed*",
|
||||
fields: [
|
||||
...(type === "error" && errorMessage
|
||||
? [
|
||||
{
|
||||
title: "Error Message",
|
||||
value: errorMessage,
|
||||
short: false,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: "Project",
|
||||
value: projectName,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Application",
|
||||
value: applicationName,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Type",
|
||||
value: databaseType,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Database Name",
|
||||
value: databaseName,
|
||||
},
|
||||
{
|
||||
title: "Time",
|
||||
value: date.toLocaleString(),
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Type",
|
||||
value: type,
|
||||
},
|
||||
{
|
||||
title: "Status",
|
||||
value: type === "success" ? "Successful" : "Failed",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (slack) {
|
||||
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 (lark) {
|
||||
const limitCharacter = 800;
|
||||
const truncatedErrorMessage =
|
||||
errorMessage && errorMessage.length > limitCharacter
|
||||
? errorMessage.substring(0, limitCharacter)
|
||||
: errorMessage;
|
||||
|
||||
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",
|
||||
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,
|
||||
},
|
||||
],
|
||||
header: {
|
||||
title: {
|
||||
tag: "plain_text",
|
||||
content:
|
||||
type === "success"
|
||||
? "✅ Database Backup Successful"
|
||||
: "❌ Database Backup Failed",
|
||||
},
|
||||
...(type === "error" && truncatedErrorMessage
|
||||
? [
|
||||
subtitle: {
|
||||
tag: "plain_text",
|
||||
content: "",
|
||||
},
|
||||
template: type === "success" ? "green" : "red",
|
||||
padding: "12px 12px 12px 12px",
|
||||
},
|
||||
body: {
|
||||
direction: "vertical",
|
||||
padding: "12px 12px 12px 12px",
|
||||
elements: [
|
||||
{
|
||||
tag: "column_set",
|
||||
columns: [
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Error Message:**\n\`\`\`\n${truncatedErrorMessage}\n\`\`\``,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
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",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -7,8 +7,8 @@ import { and, eq } from "drizzle-orm";
|
||||
import {
|
||||
sendDiscordNotification,
|
||||
sendEmailNotification,
|
||||
sendLarkNotification,
|
||||
sendGotifyNotification,
|
||||
sendLarkNotification,
|
||||
sendNtfyNotification,
|
||||
sendSlackNotification,
|
||||
sendTelegramNotification,
|
||||
@@ -39,182 +39,185 @@ export const sendDockerCleanupNotifications = async (
|
||||
for (const notification of notificationList) {
|
||||
const { email, discord, telegram, slack, gotify, ntfy, 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: `<t:${unixDate}:D>`,
|
||||
inline: true,
|
||||
await sendDiscordNotification(discord, {
|
||||
title: decorate(">", "`✅` Docker Cleanup"),
|
||||
color: 0x57f287,
|
||||
fields: [
|
||||
{
|
||||
name: decorate("`📅`", "Date"),
|
||||
value: `<t:${unixDate}:D>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`⌚`", "Time"),
|
||||
value: `<t:${unixDate}:t>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`❓`", "Type"),
|
||||
value: "Successful",
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`📜`", "Message"),
|
||||
value: `\`\`\`${message}\`\`\``,
|
||||
},
|
||||
],
|
||||
timestamp: date.toISOString(),
|
||||
footer: {
|
||||
text: "Dokploy Docker Cleanup Notification",
|
||||
},
|
||||
{
|
||||
name: decorate("`⌚`", "Time"),
|
||||
value: `<t:${unixDate}:t>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`❓`", "Type"),
|
||||
value: "Successful",
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`📜`", "Message"),
|
||||
value: `\`\`\`${message}\`\`\``,
|
||||
},
|
||||
],
|
||||
timestamp: date.toISOString(),
|
||||
footer: {
|
||||
text: "Dokploy Docker Cleanup Notification",
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (gotify) {
|
||||
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,
|
||||
`<b>✅ Docker Cleanup</b>\n\n<b>Message:</b> ${message}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`,
|
||||
);
|
||||
}
|
||||
if (telegram) {
|
||||
await sendTelegramNotification(
|
||||
telegram,
|
||||
`<b>✅ Docker Cleanup</b>\n\n<b>Message:</b> ${message}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`,
|
||||
);
|
||||
}
|
||||
|
||||
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 (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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -7,8 +7,8 @@ import { eq } from "drizzle-orm";
|
||||
import {
|
||||
sendDiscordNotification,
|
||||
sendEmailNotification,
|
||||
sendLarkNotification,
|
||||
sendGotifyNotification,
|
||||
sendLarkNotification,
|
||||
sendNtfyNotification,
|
||||
sendSlackNotification,
|
||||
sendTelegramNotification,
|
||||
@@ -34,18 +34,23 @@ export const sendDokployRestartNotifications = async () => {
|
||||
const { email, discord, telegram, slack, gotify, ntfy, 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,
|
||||
);
|
||||
}
|
||||
|
||||
if (discord) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${discord.decoration ? decoration : ""} ${text}`.trim();
|
||||
|
||||
try {
|
||||
await sendDiscordNotification(discord, {
|
||||
title: decorate(">", "`✅` Dokploy Server Restarted"),
|
||||
color: 0x57f287,
|
||||
@@ -71,27 +76,19 @@ export const sendDokployRestartNotifications = async () => {
|
||||
text: "Dokploy Restart Notification",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (gotify) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${gotify.decoration ? decoration : ""} ${text}\n`;
|
||||
try {
|
||||
if (gotify) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${gotify.decoration ? decoration : ""} ${text}\n`;
|
||||
await sendGotifyNotification(
|
||||
gotify,
|
||||
decorate("✅", "Dokploy Server Restarted"),
|
||||
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (ntfy) {
|
||||
try {
|
||||
if (ntfy) {
|
||||
await sendNtfyNotification(
|
||||
ntfy,
|
||||
"Dokploy Server Restarted",
|
||||
@@ -99,25 +96,17 @@ export const sendDokployRestartNotifications = async () => {
|
||||
"",
|
||||
`🕒Date: ${date.toLocaleString()}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (telegram) {
|
||||
try {
|
||||
if (telegram) {
|
||||
await sendTelegramNotification(
|
||||
telegram,
|
||||
`<b>✅ Dokploy Server Restarted</b>\n\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (slack) {
|
||||
const { channel } = slack;
|
||||
try {
|
||||
if (slack) {
|
||||
const { channel } = slack;
|
||||
await sendSlackNotification(slack, {
|
||||
channel: channel,
|
||||
attachments: [
|
||||
@@ -134,13 +123,9 @@ export const sendDokployRestartNotifications = async () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (lark) {
|
||||
try {
|
||||
if (lark) {
|
||||
await sendLarkNotification(lark, {
|
||||
msg_type: "interactive",
|
||||
card: {
|
||||
@@ -182,7 +167,7 @@ export const sendDokployRestartNotifications = async () => {
|
||||
elements: [
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Status:**\nSuccessful`,
|
||||
content: "**Status:**\nSuccessful",
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
@@ -210,9 +195,9 @@ export const sendDokployRestartNotifications = async () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type {
|
||||
discord,
|
||||
email,
|
||||
lark,
|
||||
gotify,
|
||||
lark,
|
||||
ntfy,
|
||||
slack,
|
||||
telegram,
|
||||
@@ -38,6 +38,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"}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -45,15 +48,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 (
|
||||
@@ -90,13 +101,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"}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -140,7 +159,9 @@ export const sendNtfyNotification = async (
|
||||
const response = await fetch(`${connection.serverUrl}/${connection.topic}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${connection.accessToken}`,
|
||||
...(connection.accessToken && {
|
||||
Authorization: `Bearer ${connection.accessToken}`,
|
||||
}),
|
||||
"X-Priority": connection.priority?.toString() || "3",
|
||||
"X-Title": title,
|
||||
"X-Tags": tags,
|
||||
|
||||
55
packages/server/src/utils/process/ExecError.ts
Normal file
55
packages/server/src/utils/process/ExecError.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
export interface ExecErrorDetails {
|
||||
command: string;
|
||||
stdout?: string;
|
||||
stderr?: string;
|
||||
exitCode?: number;
|
||||
originalError?: Error;
|
||||
serverId?: string | null;
|
||||
}
|
||||
|
||||
export class ExecError extends Error {
|
||||
public readonly command: string;
|
||||
public readonly stdout?: string;
|
||||
public readonly stderr?: string;
|
||||
public readonly exitCode?: number;
|
||||
public readonly originalError?: Error;
|
||||
public readonly serverId?: string | null;
|
||||
|
||||
constructor(message: string, details: ExecErrorDetails) {
|
||||
super(message);
|
||||
this.name = "ExecError";
|
||||
this.command = details.command;
|
||||
this.stdout = details.stdout;
|
||||
this.stderr = details.stderr;
|
||||
this.exitCode = details.exitCode;
|
||||
this.originalError = details.originalError;
|
||||
this.serverId = details.serverId;
|
||||
|
||||
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this, ExecError);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a formatted error message with all details
|
||||
*/
|
||||
getDetailedMessage(): string {
|
||||
const parts = [
|
||||
`Command: ${this.command}`,
|
||||
this.exitCode !== undefined ? `Exit Code: ${this.exitCode}` : null,
|
||||
this.serverId ? `Server ID: ${this.serverId}` : "Location: Local",
|
||||
this.stderr ? `Stderr: ${this.stderr}` : null,
|
||||
this.stdout ? `Stdout: ${this.stdout}` : null,
|
||||
].filter(Boolean);
|
||||
|
||||
return `${this.message}\n${parts.join("\n")}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this error is from a remote execution
|
||||
*/
|
||||
isRemote(): boolean {
|
||||
return !!this.serverId;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user