mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-27 10:05:32 +02:00
Merge branch 'canary' into Better-deployment-logs-for-long-commit-message-#2973
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import {
|
||||
deployRemoteApplication,
|
||||
deployRemoteCompose,
|
||||
deployRemotePreviewApplication,
|
||||
rebuildRemoteApplication,
|
||||
rebuildRemoteCompose,
|
||||
deployApplication,
|
||||
deployCompose,
|
||||
deployPreviewApplication,
|
||||
rebuildApplication,
|
||||
rebuildCompose,
|
||||
updateApplicationStatus,
|
||||
updateCompose,
|
||||
updatePreviewDeployment,
|
||||
@@ -16,13 +16,13 @@ export const deploy = async (job: DeployJob) => {
|
||||
await updateApplicationStatus(job.applicationId, "running");
|
||||
if (job.server) {
|
||||
if (job.type === "redeploy") {
|
||||
await rebuildRemoteApplication({
|
||||
await rebuildApplication({
|
||||
applicationId: job.applicationId,
|
||||
titleLog: job.titleLog || "Rebuild deployment",
|
||||
descriptionLog: job.descriptionLog || "",
|
||||
});
|
||||
} else if (job.type === "deploy") {
|
||||
await deployRemoteApplication({
|
||||
await deployApplication({
|
||||
applicationId: job.applicationId,
|
||||
titleLog: job.titleLog || "Manual deployment",
|
||||
descriptionLog: job.descriptionLog || "",
|
||||
@@ -36,13 +36,13 @@ export const deploy = async (job: DeployJob) => {
|
||||
|
||||
if (job.server) {
|
||||
if (job.type === "redeploy") {
|
||||
await rebuildRemoteCompose({
|
||||
await rebuildCompose({
|
||||
composeId: job.composeId,
|
||||
titleLog: job.titleLog || "Rebuild deployment",
|
||||
descriptionLog: job.descriptionLog || "",
|
||||
});
|
||||
} else if (job.type === "deploy") {
|
||||
await deployRemoteCompose({
|
||||
await deployCompose({
|
||||
composeId: job.composeId,
|
||||
titleLog: job.titleLog || "Manual deployment",
|
||||
descriptionLog: job.descriptionLog || "",
|
||||
@@ -55,7 +55,7 @@ export const deploy = async (job: DeployJob) => {
|
||||
});
|
||||
if (job.server) {
|
||||
if (job.type === "deploy") {
|
||||
await deployRemotePreviewApplication({
|
||||
await deployPreviewApplication({
|
||||
applicationId: job.applicationId,
|
||||
titleLog: job.titleLog || "Preview Deployment",
|
||||
descriptionLog: job.descriptionLog || "",
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { extractCommitMessage } from "@/pages/api/deploy/[refreshToken]";
|
||||
import {
|
||||
extractCommitMessage,
|
||||
extractImageName,
|
||||
extractImageTag,
|
||||
extractImageTagFromRequest,
|
||||
} from "@/pages/api/deploy/[refreshToken]";
|
||||
|
||||
describe("GitHub Webhook Skip CI", () => {
|
||||
const mockGithubHeaders = {
|
||||
@@ -96,3 +101,308 @@ describe("GitHub Webhook Skip CI", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GitHub Packages Docker Image Tag Extraction", () => {
|
||||
it("should extract tag from container_metadata", () => {
|
||||
const headers = { "x-github-event": "registry_package" };
|
||||
const body = {
|
||||
registry_package: {
|
||||
package_version: {
|
||||
version: "sha256:abc123...",
|
||||
container_metadata: {
|
||||
tag: {
|
||||
name: "v1.0.0",
|
||||
digest: "sha256:abc123...",
|
||||
},
|
||||
},
|
||||
package_url: "ghcr.io/owner/repo:v1.0.0",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const tag = extractImageTagFromRequest(headers, body);
|
||||
expect(tag).toBe("v1.0.0");
|
||||
});
|
||||
|
||||
it("should extract tag from package_url when container_metadata tag matches version", () => {
|
||||
const headers = { "x-github-event": "registry_package" };
|
||||
const body = {
|
||||
registry_package: {
|
||||
package_version: {
|
||||
version: "sha256:abc123...",
|
||||
container_metadata: {
|
||||
tag: {
|
||||
name: "sha256:abc123...",
|
||||
digest: "sha256:abc123...",
|
||||
},
|
||||
},
|
||||
package_url: "ghcr.io/owner/repo:latest",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const tag = extractImageTagFromRequest(headers, body);
|
||||
expect(tag).toBe("latest");
|
||||
});
|
||||
|
||||
it("should extract tag from package_url when container_metadata is missing", () => {
|
||||
const headers = { "x-github-event": "registry_package" };
|
||||
const body = {
|
||||
registry_package: {
|
||||
package_version: {
|
||||
version: "sha256:abc123...",
|
||||
package_url: "ghcr.io/owner/repo:1.2.3",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const tag = extractImageTagFromRequest(headers, body);
|
||||
expect(tag).toBe("1.2.3");
|
||||
});
|
||||
|
||||
it("should handle different tag formats in package_url", () => {
|
||||
const headers = { "x-github-event": "registry_package" };
|
||||
const testCases = [
|
||||
{ url: "ghcr.io/owner/repo:latest", expected: "latest" },
|
||||
{ url: "ghcr.io/owner/repo:v1.0.0", expected: "v1.0.0" },
|
||||
{ url: "ghcr.io/owner/repo:1.2.3", expected: "1.2.3" },
|
||||
{ url: "ghcr.io/owner/repo:dev", expected: "dev" },
|
||||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
const body = {
|
||||
registry_package: {
|
||||
package_version: {
|
||||
version: "sha256:abc123...",
|
||||
package_url: testCase.url,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const tag = extractImageTagFromRequest(headers, body);
|
||||
expect(tag).toBe(testCase.expected);
|
||||
}
|
||||
});
|
||||
|
||||
it("should return null for non-registry_package events", () => {
|
||||
const headers = { "x-github-event": "push" };
|
||||
const body = {
|
||||
registry_package: {
|
||||
package_version: {
|
||||
package_url: "ghcr.io/owner/repo:latest",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const tag = extractImageTagFromRequest(headers, body);
|
||||
expect(tag).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null when package_version is missing", () => {
|
||||
const headers = { "x-github-event": "registry_package" };
|
||||
const body = {
|
||||
registry_package: {},
|
||||
};
|
||||
|
||||
const tag = extractImageTagFromRequest(headers, body);
|
||||
expect(tag).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null when package_url has no tag", () => {
|
||||
const headers = { "x-github-event": "registry_package" };
|
||||
const body = {
|
||||
registry_package: {
|
||||
package_version: {
|
||||
version: "sha256:abc123...",
|
||||
package_url: "ghcr.io/owner/repo",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const tag = extractImageTagFromRequest(headers, body);
|
||||
expect(tag).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null when package_url ends with colon (no tag)", () => {
|
||||
const headers = { "x-github-event": "registry_package" };
|
||||
const body = {
|
||||
registry_package: {
|
||||
package_version: {
|
||||
version: "sha256:abc123...",
|
||||
package_url: "ghcr.io/owner/repo:",
|
||||
container_metadata: {
|
||||
tag: {
|
||||
name: "",
|
||||
digest: "sha256:abc123...",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const tag = extractImageTagFromRequest(headers, body);
|
||||
expect(tag).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null when tag name is empty string", () => {
|
||||
const headers = { "x-github-event": "registry_package" };
|
||||
const body = {
|
||||
registry_package: {
|
||||
package_version: {
|
||||
version: "sha256:abc123...",
|
||||
container_metadata: {
|
||||
tag: {
|
||||
name: "",
|
||||
digest: "sha256:abc123...",
|
||||
},
|
||||
},
|
||||
package_url: "ghcr.io/owner/repo:",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const tag = extractImageTagFromRequest(headers, body);
|
||||
expect(tag).toBeNull();
|
||||
});
|
||||
|
||||
it("should ignore tag if it matches the version (digest)", () => {
|
||||
const headers = { "x-github-event": "registry_package" };
|
||||
const body = {
|
||||
registry_package: {
|
||||
package_version: {
|
||||
version: "sha256:abc123...",
|
||||
container_metadata: {
|
||||
tag: {
|
||||
name: "sha256:abc123...",
|
||||
digest: "sha256:abc123...",
|
||||
},
|
||||
},
|
||||
package_url: "ghcr.io/owner/repo:latest",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const tag = extractImageTagFromRequest(headers, body);
|
||||
expect(tag).toBe("latest");
|
||||
});
|
||||
|
||||
it("should handle registry_package commit message with package_url", () => {
|
||||
const headers = { "x-github-event": "registry_package" };
|
||||
const body = {
|
||||
registry_package: {
|
||||
package_version: {
|
||||
package_url: "ghcr.io/owner/repo:latest",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const message = extractCommitMessage(headers, body);
|
||||
expect(message).toBe("Docker GHCR image pushed: ghcr.io/owner/repo:latest");
|
||||
});
|
||||
|
||||
it("should handle registry_package commit message when package_url is missing", () => {
|
||||
const headers = { "x-github-event": "registry_package" };
|
||||
const body = {
|
||||
registry_package: {
|
||||
package_version: {
|
||||
version: "sha256:abc123...",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const message = extractCommitMessage(headers, body);
|
||||
expect(message).toBe("Docker GHCR image pushed");
|
||||
});
|
||||
|
||||
it("should handle registry_package commit message when package_version is missing", () => {
|
||||
const headers = { "x-github-event": "registry_package" };
|
||||
const body = {
|
||||
registry_package: {},
|
||||
};
|
||||
|
||||
const message = extractCommitMessage(headers, body);
|
||||
expect(message).toBe("NEW COMMIT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Docker Image Name and Tag Extraction", () => {
|
||||
describe("extractImageName", () => {
|
||||
it("should return image name without tag", () => {
|
||||
expect(extractImageName("my-image:latest")).toBe("my-image");
|
||||
expect(extractImageName("my-image:1.0.0")).toBe("my-image");
|
||||
expect(extractImageName("ghcr.io/owner/repo:latest")).toBe(
|
||||
"ghcr.io/owner/repo",
|
||||
);
|
||||
});
|
||||
|
||||
it("should return full image name when no tag is present", () => {
|
||||
expect(extractImageName("my-image")).toBe("my-image");
|
||||
expect(extractImageName("ghcr.io/owner/repo")).toBe("ghcr.io/owner/repo");
|
||||
});
|
||||
|
||||
it("should handle images with port numbers correctly", () => {
|
||||
expect(extractImageName("registry:5000/image:tag")).toBe(
|
||||
"registry:5000/image",
|
||||
);
|
||||
expect(extractImageName("localhost:5000/my-app:latest")).toBe(
|
||||
"localhost:5000/my-app",
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle complex image paths", () => {
|
||||
expect(
|
||||
extractImageName("myregistryhost:5000/fedora/httpd:version1.0"),
|
||||
).toBe("myregistryhost:5000/fedora/httpd");
|
||||
expect(extractImageName("registry.example.com:8080/ns/app:v1.2.3")).toBe(
|
||||
"registry.example.com:8080/ns/app",
|
||||
);
|
||||
});
|
||||
|
||||
it("should return null for invalid inputs", () => {
|
||||
expect(extractImageName(null)).toBeNull();
|
||||
expect(extractImageName("")).toBeNull();
|
||||
});
|
||||
|
||||
it("should handle edge cases with multiple colons", () => {
|
||||
expect(extractImageName("image:tag:extra")).toBe("image:tag");
|
||||
expect(extractImageName("registry:5000:invalid")).toBe("registry:5000");
|
||||
});
|
||||
});
|
||||
|
||||
describe("extractImageTag", () => {
|
||||
it("should extract tag from image with tag", () => {
|
||||
expect(extractImageTag("my-image:latest")).toBe("latest");
|
||||
expect(extractImageTag("my-image:1.0.0")).toBe("1.0.0");
|
||||
expect(extractImageTag("ghcr.io/owner/repo:v1.2.3")).toBe("v1.2.3");
|
||||
});
|
||||
|
||||
it("should return 'latest' when no tag is present", () => {
|
||||
expect(extractImageTag("my-image")).toBe("latest");
|
||||
expect(extractImageTag("ghcr.io/owner/repo")).toBe("latest");
|
||||
});
|
||||
|
||||
it("should handle complex image paths with tags", () => {
|
||||
expect(
|
||||
extractImageTag("myregistryhost:5000/fedora/httpd:version1.0"),
|
||||
).toBe("version1.0");
|
||||
expect(extractImageTag("registry.example.com:8080/ns/app:v1.2.3")).toBe(
|
||||
"v1.2.3",
|
||||
);
|
||||
});
|
||||
|
||||
it("should return null for invalid inputs", () => {
|
||||
expect(extractImageTag(null)).toBeNull();
|
||||
expect(extractImageTag("")).toBeNull();
|
||||
});
|
||||
|
||||
it("should handle edge cases with multiple colons", () => {
|
||||
expect(extractImageTag("image:tag:extra")).toBe("extra");
|
||||
expect(extractImageTag("registry:5000/image:tag")).toBe("tag");
|
||||
});
|
||||
|
||||
it("should handle numeric tags", () => {
|
||||
expect(extractImageTag("my-image:123")).toBe("123");
|
||||
expect(extractImageTag("my-image:1")).toBe("1");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { debounce } from "lodash";
|
||||
import _ from "lodash";
|
||||
import {
|
||||
CheckIcon,
|
||||
ChevronsUpDown,
|
||||
@@ -236,7 +236,7 @@ export const RestoreBackup = ({
|
||||
const currentDatabaseType = form.watch("databaseType");
|
||||
const metadata = form.watch("metadata");
|
||||
|
||||
const debouncedSetSearch = debounce((value: string) => {
|
||||
const debouncedSetSearch = _.debounce((value: string) => {
|
||||
setDebouncedSearchTerm(value);
|
||||
}, 350);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FancyAnsi } from "fancy-ansi";
|
||||
import { escapeRegExp } from "lodash";
|
||||
import _ from "lodash";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Tooltip,
|
||||
@@ -47,7 +47,7 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) {
|
||||
}
|
||||
|
||||
const htmlContent = fancyAnsi.toHtml(text);
|
||||
const searchRegex = new RegExp(`(${escapeRegExp(term)})`, "gi");
|
||||
const searchRegex = new RegExp(`(${_.escapeRegExp(term)})`, "gi");
|
||||
|
||||
const modifiedContent = htmlContent.replace(
|
||||
searchRegex,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { findEnvironmentById } from "@dokploy/server/index";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
@@ -27,12 +26,10 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
import { api, type RouterOutputs } from "@/utils/api";
|
||||
|
||||
type Environment = Omit<
|
||||
Awaited<ReturnType<typeof findEnvironmentById>>,
|
||||
"project"
|
||||
>;
|
||||
type Project = RouterOutputs["project"]["all"][number];
|
||||
type Environment = Project["environments"][number];
|
||||
|
||||
export type Services = {
|
||||
appName: string;
|
||||
@@ -53,17 +50,16 @@ export type Services = {
|
||||
};
|
||||
|
||||
export const extractServices = (data: Environment | undefined) => {
|
||||
const applications: Services[] =
|
||||
data?.applications.map((item) => ({
|
||||
appName: item.appName,
|
||||
name: item.name,
|
||||
type: "application",
|
||||
id: item.applicationId,
|
||||
createdAt: item.createdAt,
|
||||
status: item.applicationStatus,
|
||||
description: item.description,
|
||||
serverId: item.serverId,
|
||||
})) || [];
|
||||
const applications: Services[] = (data?.applications?.map((item) => ({
|
||||
appName: item.appName,
|
||||
name: item.name,
|
||||
type: "application",
|
||||
id: item.applicationId,
|
||||
createdAt: item.createdAt,
|
||||
status: item.applicationStatus,
|
||||
description: item.description,
|
||||
serverId: item.serverId,
|
||||
})) ?? []) as Services[];
|
||||
|
||||
const mariadb: Services[] =
|
||||
data?.mariadb.map((item) => ({
|
||||
@@ -125,17 +121,16 @@ export const extractServices = (data: Environment | undefined) => {
|
||||
serverId: item.serverId,
|
||||
})) || [];
|
||||
|
||||
const compose: Services[] =
|
||||
data?.compose.map((item) => ({
|
||||
appName: item.appName,
|
||||
name: item.name,
|
||||
type: "compose",
|
||||
id: item.composeId,
|
||||
createdAt: item.createdAt,
|
||||
status: item.composeStatus,
|
||||
description: item.description,
|
||||
serverId: item.serverId,
|
||||
})) || [];
|
||||
const compose: Services[] = (data?.compose?.map((item) => ({
|
||||
appName: item.appName,
|
||||
name: item.name,
|
||||
type: "compose",
|
||||
id: item.composeId,
|
||||
createdAt: item.createdAt,
|
||||
status: item.composeStatus,
|
||||
description: item.description,
|
||||
serverId: item.serverId,
|
||||
})) ?? []) as Services[];
|
||||
|
||||
applications.push(
|
||||
...mysql,
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
"next": "^15.3.2",
|
||||
"next-i18next": "^15.4.2",
|
||||
"next-themes": "^0.2.1",
|
||||
"node-os-utils": "1.3.7",
|
||||
"node-os-utils": "2.0.1",
|
||||
"node-pty": "1.0.0",
|
||||
"node-schedule": "2.1.1",
|
||||
"nodemailer": "6.9.14",
|
||||
@@ -163,7 +163,6 @@
|
||||
"@types/lodash": "4.17.4",
|
||||
"@types/micromatch": "4.0.9",
|
||||
"@types/node": "^18.19.104",
|
||||
"@types/node-os-utils": "1.3.4",
|
||||
"@types/node-schedule": "2.1.6",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
|
||||
@@ -12,6 +12,17 @@ import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||
import { myQueue } from "@/server/queues/queueSetup";
|
||||
import { deploy } from "@/server/utils/deploy";
|
||||
|
||||
/**
|
||||
* Helper function to get package_version from registry_package events
|
||||
*/
|
||||
const getPackageVersion = (headers: any, body: any) => {
|
||||
const event = headers["x-github-event"];
|
||||
if (event === "registry_package") {
|
||||
return body.registry_package?.package_version;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
@@ -46,21 +57,60 @@ export default async function handler(
|
||||
}
|
||||
|
||||
const deploymentTitle = extractCommitMessage(req.headers, req.body);
|
||||
const deploymentHash = extractHash(req.headers, req.body);
|
||||
|
||||
const deploymentHash = extractHash(req.headers, req.body);
|
||||
const sourceType = application.sourceType;
|
||||
|
||||
if (sourceType === "docker") {
|
||||
const applicationImageName = extractImageName(application.dockerImage);
|
||||
const applicationDockerTag = extractImageTag(application.dockerImage);
|
||||
|
||||
const webhookImageName = extractImageNameFromRequest(
|
||||
req.headers,
|
||||
req.body,
|
||||
);
|
||||
const webhookDockerTag = extractImageTagFromRequest(
|
||||
req.headers,
|
||||
req.body,
|
||||
);
|
||||
if (
|
||||
applicationDockerTag &&
|
||||
webhookDockerTag &&
|
||||
webhookDockerTag !== applicationDockerTag
|
||||
) {
|
||||
|
||||
if (!applicationImageName) {
|
||||
res.status(301).json({
|
||||
message: "Application Docker Image Name Not Found",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!webhookImageName) {
|
||||
res.status(301).json({
|
||||
message: "Webhook Docker Image Name Not Found",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate image name matches
|
||||
if (webhookImageName !== applicationImageName) {
|
||||
res.status(301).json({
|
||||
message: `Application Image Name (${applicationImageName}) doesn't match request event payload Image Name (${webhookImageName}).`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!applicationDockerTag) {
|
||||
res.status(301).json({
|
||||
message: "Application Docker Tag Not Found",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!webhookDockerTag) {
|
||||
res.status(301).json({
|
||||
message: "Webhook Docker Tag Not Found",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (webhookDockerTag !== applicationDockerTag) {
|
||||
res.status(301).json({
|
||||
message: `Application Image Tag (${applicationDockerTag}) doesn't match request event payload Image Tag (${webhookDockerTag}).`,
|
||||
});
|
||||
@@ -191,7 +241,7 @@ export default async function handler(
|
||||
const jobData: DeploymentJob = {
|
||||
applicationId: application.applicationId as string,
|
||||
titleLog: deploymentTitle,
|
||||
descriptionLog: `Hash: ${deploymentHash}`,
|
||||
...(deploymentHash && { descriptionLog: `Hash: ${deploymentHash}` }),
|
||||
type: "deploy",
|
||||
applicationType: "application",
|
||||
server: !!application.serverId,
|
||||
@@ -222,6 +272,39 @@ export default async function handler(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the image name without the tag
|
||||
* Example: "my-image" => "my-image"
|
||||
* Example: "my-image:latest" => "my-image"
|
||||
* Example: "my-image:1.0.0" => "my-image"
|
||||
* Example: "myregistryhost:5000/fedora/httpd:version1.0" => "myregistryhost:5000/fedora/httpd"
|
||||
* @link https://docs.docker.com/reference/cli/docker/image/tag/
|
||||
*/
|
||||
export function extractImageName(dockerImage: string | null): string | null {
|
||||
if (!dockerImage || typeof dockerImage !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Handle case where there's no tag (no colon or colon is part of port number)
|
||||
const lastColonIndex = dockerImage.lastIndexOf(":");
|
||||
if (lastColonIndex === -1) {
|
||||
return dockerImage;
|
||||
}
|
||||
|
||||
// Check if the part after the last colon looks like a tag (not a port number)
|
||||
// Port numbers are typically 1-5 digits, tags are usually longer or contain letters
|
||||
const afterColon = dockerImage.substring(lastColonIndex + 1);
|
||||
const isPortNumber = /^\d{1,5}$/.test(afterColon);
|
||||
|
||||
// If it's a port number (like registry:5000/image), don't split
|
||||
if (isPortNumber) {
|
||||
return dockerImage;
|
||||
}
|
||||
|
||||
// Otherwise, split at the last colon to get image name
|
||||
return dockerImage.substring(0, lastColonIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the last part of the image name, which is the tag
|
||||
* Example: "my-image" => null
|
||||
@@ -230,7 +313,7 @@ export default async function handler(
|
||||
* Example: "myregistryhost:5000/fedora/httpd:version1.0" => "version1.0"
|
||||
* @link https://docs.docker.com/reference/cli/docker/image/tag/
|
||||
*/
|
||||
function extractImageTag(dockerImage: string | null) {
|
||||
export function extractImageTag(dockerImage: string | null) {
|
||||
if (!dockerImage || typeof dockerImage !== "string") {
|
||||
return null;
|
||||
}
|
||||
@@ -240,12 +323,78 @@ function extractImageTag(dockerImage: string | null) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the image name (without tag) from webhook request
|
||||
* @link https://docs.docker.com/docker-hub/webhooks/#example-webhook-payload
|
||||
* @link https://docs.github.com/en/webhooks/webhook-events-and-payloads#registry_package
|
||||
*/
|
||||
export const extractImageNameFromRequest = (
|
||||
headers: any,
|
||||
body: any,
|
||||
): string | null => {
|
||||
// GitHub Packages: registry_package events (container registry)
|
||||
const packageVersion = getPackageVersion(headers, body);
|
||||
if (packageVersion?.package_url) {
|
||||
const packageUrl = packageVersion.package_url;
|
||||
// Remove tag if present (everything after the last colon)
|
||||
if (packageUrl.includes(":")) {
|
||||
const lastColonIndex = packageUrl.lastIndexOf(":");
|
||||
// Check if it's a port number (like registry:5000/image)
|
||||
const afterColon = packageUrl.substring(lastColonIndex + 1);
|
||||
const isPortNumber = /^\d{1,5}$/.test(afterColon);
|
||||
if (isPortNumber) {
|
||||
return packageUrl;
|
||||
}
|
||||
return packageUrl.substring(0, lastColonIndex);
|
||||
}
|
||||
return packageUrl;
|
||||
}
|
||||
|
||||
// Docker Hub
|
||||
if (headers["user-agent"]?.includes("Go-http-client")) {
|
||||
if (body.repository) {
|
||||
const repoName = body.repository.repo_name;
|
||||
return `${repoName}`;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* @link https://docs.docker.com/docker-hub/webhooks/#example-webhook-payload
|
||||
* @link https://docs.github.com/en/webhooks/webhook-events-and-payloads#registry_package
|
||||
*/
|
||||
export const extractImageTagFromRequest = (
|
||||
headers: any,
|
||||
body: any,
|
||||
): string | null => {
|
||||
// GitHub Packages: registry_package events (container registry)
|
||||
const packageVersion = getPackageVersion(headers, body);
|
||||
if (packageVersion) {
|
||||
// Try to get tag from container_metadata first (most reliable)
|
||||
// Only use it if it's not empty and not the same as the version (digest)
|
||||
const tagName = packageVersion.container_metadata?.tag?.name?.trim() || "";
|
||||
if (
|
||||
tagName &&
|
||||
tagName !== packageVersion.version &&
|
||||
!tagName.startsWith("sha256:")
|
||||
) {
|
||||
return tagName;
|
||||
}
|
||||
// Fallback: extract tag from package_url (e.g., "ghcr.io/owner/repo:tag")
|
||||
if (packageVersion.package_url) {
|
||||
const packageUrl = packageVersion.package_url;
|
||||
// Handle case where package_url ends with colon (no tag)
|
||||
if (packageUrl.endsWith(":")) {
|
||||
return null;
|
||||
}
|
||||
const tagMatch = packageUrl.match(/:([^:]+)$/);
|
||||
if (tagMatch?.[1]?.trim()) {
|
||||
return tagMatch[1].trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Docker Hub
|
||||
if (headers["user-agent"]?.includes("Go-http-client")) {
|
||||
if (body.push_data && body.repository) {
|
||||
return body.push_data.tag;
|
||||
@@ -255,6 +404,18 @@ export const extractImageTagFromRequest = (
|
||||
};
|
||||
|
||||
export const extractCommitMessage = (headers: any, body: any) => {
|
||||
// GitHub Packages: registry_package events (container tags)
|
||||
const githubEvent = headers["x-github-event"];
|
||||
if (githubEvent === "registry_package") {
|
||||
const packageVersion = getPackageVersion(headers, body);
|
||||
if (packageVersion) {
|
||||
if (packageVersion.package_url) {
|
||||
return `Docker GHCR image pushed: ${packageVersion.package_url}`;
|
||||
}
|
||||
return "Docker GHCR image pushed";
|
||||
}
|
||||
// If package_version is missing, fall through to default behavior
|
||||
}
|
||||
// GitHub
|
||||
if (headers["x-github-event"]) {
|
||||
return body.head_commit ? body.head_commit.message : "NEW COMMIT";
|
||||
@@ -283,7 +444,7 @@ export const extractCommitMessage = (headers: any, body: any) => {
|
||||
|
||||
if (headers["user-agent"]?.includes("Go-http-client")) {
|
||||
if (body.push_data && body.repository) {
|
||||
return `Docker image pushed: ${body.repository.repo_name}:${body.push_data.tag} by ${body.push_data.pusher}`;
|
||||
return `DockerHub image pushed: ${body.repository.repo_name}:${body.push_data.tag} by ${body.push_data.pusher}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -115,6 +115,7 @@ export type Services = {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
status?: "idle" | "running" | "done" | "error";
|
||||
lastDeployDate?: Date | null;
|
||||
};
|
||||
|
||||
type Project = Awaited<ReturnType<typeof findProjectById>>;
|
||||
@@ -128,16 +129,34 @@ export const extractServicesFromEnvironment = (
|
||||
const allServices: Services[] = [];
|
||||
|
||||
const applications: Services[] =
|
||||
environment.applications?.map((item) => ({
|
||||
appName: item.appName,
|
||||
name: item.name,
|
||||
type: "application",
|
||||
id: item.applicationId,
|
||||
createdAt: item.createdAt,
|
||||
status: item.applicationStatus,
|
||||
description: item.description,
|
||||
serverId: item.serverId,
|
||||
})) || [];
|
||||
environment.applications?.map((item) => {
|
||||
// Get the most recent deployment date
|
||||
let lastDeployDate: Date | null = null;
|
||||
const deployments = (item as any).deployments;
|
||||
if (deployments && deployments.length > 0) {
|
||||
for (const deployment of deployments) {
|
||||
const deployDate = new Date(
|
||||
deployment.finishedAt ||
|
||||
deployment.startedAt ||
|
||||
deployment.createdAt,
|
||||
);
|
||||
if (!lastDeployDate || deployDate > lastDeployDate) {
|
||||
lastDeployDate = deployDate;
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
appName: item.appName,
|
||||
name: item.name,
|
||||
type: "application",
|
||||
id: item.applicationId,
|
||||
createdAt: item.createdAt,
|
||||
status: item.applicationStatus,
|
||||
description: item.description,
|
||||
serverId: item.serverId,
|
||||
lastDeployDate,
|
||||
};
|
||||
}) || [];
|
||||
|
||||
const mariadb: Services[] =
|
||||
environment.mariadb?.map((item) => ({
|
||||
@@ -200,16 +219,34 @@ export const extractServicesFromEnvironment = (
|
||||
})) || [];
|
||||
|
||||
const compose: Services[] =
|
||||
environment.compose?.map((item) => ({
|
||||
appName: item.appName,
|
||||
name: item.name,
|
||||
type: "compose",
|
||||
id: item.composeId,
|
||||
createdAt: item.createdAt,
|
||||
status: item.composeStatus,
|
||||
description: item.description,
|
||||
serverId: item.serverId,
|
||||
})) || [];
|
||||
environment.compose?.map((item) => {
|
||||
// Get the most recent deployment date
|
||||
let lastDeployDate: Date | null = null;
|
||||
const deployments = (item as any).deployments;
|
||||
if (deployments && deployments.length > 0) {
|
||||
for (const deployment of deployments) {
|
||||
const deployDate = new Date(
|
||||
deployment.finishedAt ||
|
||||
deployment.startedAt ||
|
||||
deployment.createdAt,
|
||||
);
|
||||
if (!lastDeployDate || deployDate > lastDeployDate) {
|
||||
lastDeployDate = deployDate;
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
appName: item.appName,
|
||||
name: item.name,
|
||||
type: "compose",
|
||||
id: item.composeId,
|
||||
createdAt: item.createdAt,
|
||||
status: item.composeStatus,
|
||||
description: item.description,
|
||||
serverId: item.serverId,
|
||||
lastDeployDate,
|
||||
};
|
||||
}) || [];
|
||||
|
||||
allServices.push(
|
||||
...applications,
|
||||
@@ -237,9 +274,9 @@ const EnvironmentPage = (
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const [sortBy, setSortBy] = useState<string>(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
return localStorage.getItem("servicesSort") || "createdAt-desc";
|
||||
return localStorage.getItem("servicesSort") || "lastDeploy-desc";
|
||||
}
|
||||
return "createdAt-desc";
|
||||
return "lastDeploy-desc";
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -261,10 +298,45 @@ const EnvironmentPage = (
|
||||
comparison =
|
||||
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
||||
break;
|
||||
case "lastDeploy": {
|
||||
const aLastDeploy = a.lastDeployDate;
|
||||
const bLastDeploy = b.lastDeployDate;
|
||||
|
||||
if (direction === "desc") {
|
||||
// For "desc" (newest first): services with deployments first, then those without
|
||||
if (!aLastDeploy && !bLastDeploy) {
|
||||
comparison = 0;
|
||||
} else if (!aLastDeploy) {
|
||||
comparison = 1; // a (no deploy) goes after b (has deploy)
|
||||
} else if (!bLastDeploy) {
|
||||
comparison = -1; // a (has deploy) goes before b (no deploy)
|
||||
} else {
|
||||
// Both have deployments: newest first (negative if a is newer)
|
||||
comparison = bLastDeploy.getTime() - aLastDeploy.getTime();
|
||||
}
|
||||
} else {
|
||||
// For "asc" (oldest first): services with deployments first, then those without
|
||||
if (!aLastDeploy && !bLastDeploy) {
|
||||
comparison = 0;
|
||||
} else if (!aLastDeploy) {
|
||||
comparison = 1; // a (no deploy) goes after b (has deploy)
|
||||
} else if (!bLastDeploy) {
|
||||
comparison = -1; // a (has deploy) goes before b (no deploy)
|
||||
} else {
|
||||
// Both have deployments: oldest first
|
||||
comparison = aLastDeploy.getTime() - bLastDeploy.getTime();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
comparison = 0;
|
||||
}
|
||||
return direction === "asc" ? comparison : -comparison;
|
||||
// For other fields, apply direction normally
|
||||
if (field !== "lastDeploy") {
|
||||
return direction === "asc" ? comparison : -comparison;
|
||||
}
|
||||
return comparison;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1217,6 +1289,9 @@ const EnvironmentPage = (
|
||||
<SelectValue placeholder="Sort by..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="lastDeploy-desc">
|
||||
Recently deployed
|
||||
</SelectItem>
|
||||
<SelectItem value="createdAt-desc">
|
||||
Newest first
|
||||
</SelectItem>
|
||||
|
||||
@@ -3,13 +3,14 @@ import {
|
||||
addNewService,
|
||||
checkServiceAccess,
|
||||
cloneCompose,
|
||||
cloneComposeRemote,
|
||||
createCommand,
|
||||
createCompose,
|
||||
createComposeByTemplate,
|
||||
createDomain,
|
||||
createMount,
|
||||
deleteMount,
|
||||
execAsync,
|
||||
execAsyncRemote,
|
||||
findComposeById,
|
||||
findDomainsByComposeId,
|
||||
findEnvironmentById,
|
||||
@@ -302,10 +303,12 @@ export const composeRouter = createTRPCRouter({
|
||||
message: "You are not authorized to fetch this compose",
|
||||
});
|
||||
}
|
||||
|
||||
const command = await cloneCompose(compose);
|
||||
if (compose.serverId) {
|
||||
await cloneComposeRemote(compose);
|
||||
await execAsyncRemote(compose.serverId, command);
|
||||
} else {
|
||||
await cloneCompose(compose);
|
||||
await execAsync(command);
|
||||
}
|
||||
return compose.sourceType;
|
||||
} catch (err) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
findNotificationById,
|
||||
findOrganizationById,
|
||||
findUserById,
|
||||
getDokployUrl,
|
||||
getUserByToken,
|
||||
IS_CLOUD,
|
||||
removeUserById,
|
||||
@@ -419,11 +420,10 @@ export const userRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
const admin = await findAdmin();
|
||||
const host =
|
||||
process.env.NODE_ENV === "development"
|
||||
? "http://localhost:3000"
|
||||
: admin.user.host;
|
||||
: await getDokployUrl();
|
||||
const inviteLink = `${host}/invitation?token=${input.invitationId}`;
|
||||
|
||||
const organization = await findOrganizationById(
|
||||
|
||||
@@ -2,13 +2,8 @@ import {
|
||||
deployApplication,
|
||||
deployCompose,
|
||||
deployPreviewApplication,
|
||||
deployRemoteApplication,
|
||||
deployRemoteCompose,
|
||||
deployRemotePreviewApplication,
|
||||
rebuildApplication,
|
||||
rebuildCompose,
|
||||
rebuildRemoteApplication,
|
||||
rebuildRemoteCompose,
|
||||
updateApplicationStatus,
|
||||
updateCompose,
|
||||
updatePreviewDeployment,
|
||||
@@ -24,91 +19,48 @@ export const deploymentWorker = new Worker(
|
||||
if (job.data.applicationType === "application") {
|
||||
await updateApplicationStatus(job.data.applicationId, "running");
|
||||
|
||||
if (job.data.server) {
|
||||
if (job.data.type === "redeploy") {
|
||||
await rebuildRemoteApplication({
|
||||
applicationId: job.data.applicationId,
|
||||
titleLog: job.data.titleLog,
|
||||
descriptionLog: job.data.descriptionLog,
|
||||
});
|
||||
} else if (job.data.type === "deploy") {
|
||||
await deployRemoteApplication({
|
||||
applicationId: job.data.applicationId,
|
||||
titleLog: job.data.titleLog,
|
||||
descriptionLog: job.data.descriptionLog,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (job.data.type === "redeploy") {
|
||||
await rebuildApplication({
|
||||
applicationId: job.data.applicationId,
|
||||
titleLog: job.data.titleLog,
|
||||
descriptionLog: job.data.descriptionLog,
|
||||
});
|
||||
} else if (job.data.type === "deploy") {
|
||||
await deployApplication({
|
||||
applicationId: job.data.applicationId,
|
||||
titleLog: job.data.titleLog,
|
||||
descriptionLog: job.data.descriptionLog,
|
||||
});
|
||||
}
|
||||
if (job.data.type === "redeploy") {
|
||||
await rebuildApplication({
|
||||
applicationId: job.data.applicationId,
|
||||
titleLog: job.data.titleLog,
|
||||
descriptionLog: job.data.descriptionLog,
|
||||
});
|
||||
} else if (job.data.type === "deploy") {
|
||||
await deployApplication({
|
||||
applicationId: job.data.applicationId,
|
||||
titleLog: job.data.titleLog,
|
||||
descriptionLog: job.data.descriptionLog,
|
||||
});
|
||||
}
|
||||
} else if (job.data.applicationType === "compose") {
|
||||
await updateCompose(job.data.composeId, {
|
||||
composeStatus: "running",
|
||||
});
|
||||
|
||||
if (job.data.server) {
|
||||
if (job.data.type === "redeploy") {
|
||||
await rebuildRemoteCompose({
|
||||
composeId: job.data.composeId,
|
||||
titleLog: job.data.titleLog,
|
||||
descriptionLog: job.data.descriptionLog,
|
||||
});
|
||||
} else if (job.data.type === "deploy") {
|
||||
await deployRemoteCompose({
|
||||
composeId: job.data.composeId,
|
||||
titleLog: job.data.titleLog,
|
||||
descriptionLog: job.data.descriptionLog,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (job.data.type === "deploy") {
|
||||
await deployCompose({
|
||||
composeId: job.data.composeId,
|
||||
titleLog: job.data.titleLog,
|
||||
descriptionLog: job.data.descriptionLog,
|
||||
});
|
||||
} else if (job.data.type === "redeploy") {
|
||||
await rebuildCompose({
|
||||
composeId: job.data.composeId,
|
||||
titleLog: job.data.titleLog,
|
||||
descriptionLog: job.data.descriptionLog,
|
||||
});
|
||||
}
|
||||
if (job.data.type === "deploy") {
|
||||
await deployCompose({
|
||||
composeId: job.data.composeId,
|
||||
titleLog: job.data.titleLog,
|
||||
descriptionLog: job.data.descriptionLog,
|
||||
});
|
||||
} else if (job.data.type === "redeploy") {
|
||||
await rebuildCompose({
|
||||
composeId: job.data.composeId,
|
||||
titleLog: job.data.titleLog,
|
||||
descriptionLog: job.data.descriptionLog,
|
||||
});
|
||||
}
|
||||
} else if (job.data.applicationType === "application-preview") {
|
||||
await updatePreviewDeployment(job.data.previewDeploymentId, {
|
||||
previewStatus: "running",
|
||||
});
|
||||
if (job.data.server) {
|
||||
if (job.data.type === "deploy") {
|
||||
await deployRemotePreviewApplication({
|
||||
applicationId: job.data.applicationId,
|
||||
titleLog: job.data.titleLog,
|
||||
descriptionLog: job.data.descriptionLog,
|
||||
previewDeploymentId: job.data.previewDeploymentId,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (job.data.type === "deploy") {
|
||||
await deployPreviewApplication({
|
||||
applicationId: job.data.applicationId,
|
||||
titleLog: job.data.titleLog,
|
||||
descriptionLog: job.data.descriptionLog,
|
||||
previewDeploymentId: job.data.previewDeploymentId,
|
||||
});
|
||||
}
|
||||
|
||||
if (job.data.type === "deploy") {
|
||||
await deployPreviewApplication({
|
||||
applicationId: job.data.applicationId,
|
||||
titleLog: job.data.titleLog,
|
||||
descriptionLog: job.data.descriptionLog,
|
||||
previewDeploymentId: job.data.previewDeploymentId,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type http from "node:http";
|
||||
import {
|
||||
docker,
|
||||
execAsync,
|
||||
getHostSystemStats,
|
||||
getLastAdvancedStatsFile,
|
||||
recordAdvancedStats,
|
||||
validateRequest,
|
||||
@@ -49,6 +50,21 @@ export const setupDockerStatsMonitoringSocketServer = (
|
||||
}
|
||||
const intervalId = setInterval(async () => {
|
||||
try {
|
||||
// Special case: when monitoring "dokploy", get host system stats instead of container stats
|
||||
if (appName === "dokploy") {
|
||||
const stat = await getHostSystemStats();
|
||||
|
||||
await recordAdvancedStats(stat, appName);
|
||||
const data = await getLastAdvancedStatsFile(appName);
|
||||
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
data,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const filter = {
|
||||
status: ["running"],
|
||||
...(appType === "application" && {
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
await initializeNetwork();
|
||||
createDefaultTraefikConfig();
|
||||
createDefaultServerTraefikConfig();
|
||||
await execAsync("docker pull traefik:v3.5.0");
|
||||
await execAsync("docker pull traefik:v3.6.1");
|
||||
await initializeStandaloneTraefik();
|
||||
await initializeRedis();
|
||||
await initializePostgres();
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"lodash": "4.17.21",
|
||||
"micromatch": "4.0.8",
|
||||
"nanoid": "3.3.11",
|
||||
"node-os-utils": "1.3.7",
|
||||
"node-os-utils": "2.0.1",
|
||||
"node-pty": "1.0.0",
|
||||
"node-schedule": "2.1.1",
|
||||
"nodemailer": "6.9.14",
|
||||
@@ -88,7 +88,6 @@
|
||||
"@types/lodash": "4.17.4",
|
||||
"@types/micromatch": "4.0.9",
|
||||
"@types/node": "^18.19.104",
|
||||
"@types/node-os-utils": "1.3.4",
|
||||
"@types/node-schedule": "2.1.6",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { promises } from "node:fs";
|
||||
import osUtils from "node-os-utils";
|
||||
import { OSUtils } from "node-os-utils";
|
||||
import { paths } from "../constants";
|
||||
|
||||
export interface Container {
|
||||
@@ -38,22 +38,122 @@ export const recordAdvancedStats = async (
|
||||
});
|
||||
|
||||
if (appName === "dokploy") {
|
||||
const disk = await osUtils.drive.info("/");
|
||||
const osutils = new OSUtils();
|
||||
const diskResult = await osutils.disk.usageByMountPoint("/");
|
||||
|
||||
const diskUsage = disk.usedGb;
|
||||
const diskTotal = disk.totalGb;
|
||||
const diskUsedPercentage = disk.usedPercentage;
|
||||
const diskFree = disk.freeGb;
|
||||
if (diskResult.success && diskResult.data) {
|
||||
const disk = diskResult.data;
|
||||
const diskUsage = disk.used.toGB().toFixed(2);
|
||||
const diskTotal = disk.total.toGB().toFixed(2);
|
||||
const diskUsedPercentage = disk.usagePercentage;
|
||||
const diskFree = disk.available.toGB().toFixed(2);
|
||||
|
||||
await updateStatsFile(appName, "disk", {
|
||||
diskTotal: +diskTotal,
|
||||
diskUsedPercentage: +diskUsedPercentage,
|
||||
diskUsage: +diskUsage,
|
||||
diskFree: +diskFree,
|
||||
});
|
||||
await updateStatsFile(appName, "disk", {
|
||||
diskTotal: +diskTotal,
|
||||
diskUsedPercentage: +diskUsedPercentage,
|
||||
diskUsage: +diskUsage,
|
||||
diskFree: +diskFree,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get host system statistics using node-os-utils
|
||||
* This is used when monitoring "dokploy" to show host stats instead of container stats
|
||||
*/
|
||||
export const getHostSystemStats = async (): Promise<Container> => {
|
||||
const osutils = new OSUtils({
|
||||
disk: {
|
||||
includeStats: true, // Enable disk I/O statistics
|
||||
},
|
||||
});
|
||||
|
||||
// Get CPU usage
|
||||
const cpuResult = await osutils.cpu.usage();
|
||||
const cpuUsage = cpuResult.success ? cpuResult.data : 0;
|
||||
|
||||
// Get memory info
|
||||
const memResult = await osutils.memory.info();
|
||||
let memUsedGB = 0;
|
||||
let memTotalGB = 0;
|
||||
let memUsedPercent = 0;
|
||||
if (memResult.success) {
|
||||
memTotalGB = memResult.data.total.toGB();
|
||||
memUsedGB = memResult.data.used.toGB();
|
||||
memUsedPercent = memResult.data.usagePercentage;
|
||||
}
|
||||
|
||||
// Get network stats from network.overview()
|
||||
let netInputBytes = 0;
|
||||
let netOutputBytes = 0;
|
||||
const networkOverview = await osutils.network.overview();
|
||||
if (networkOverview.success) {
|
||||
netInputBytes = networkOverview.data.totalRxBytes.toBytes();
|
||||
netOutputBytes = networkOverview.data.totalTxBytes.toBytes();
|
||||
}
|
||||
|
||||
// Get Block I/O from disk.stats()
|
||||
let blockReadBytes = 0;
|
||||
let blockWriteBytes = 0;
|
||||
const diskStats = await osutils.disk.stats();
|
||||
if (diskStats.success && diskStats.data.length > 0) {
|
||||
// Filter out virtual devices (loop, ram, sr, etc.) - only include real disk devices
|
||||
const excludePatterns = [/^loop/, /^ram/, /^sr\d+$/, /^fd\d+$/];
|
||||
for (const stat of diskStats.data) {
|
||||
// Skip virtual devices
|
||||
if (
|
||||
stat.device &&
|
||||
excludePatterns.some((pattern) => pattern.test(stat.device))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
// readBytes and writeBytes are DataSize objects with .toBytes() method
|
||||
blockReadBytes += stat.readBytes.toBytes();
|
||||
blockWriteBytes += stat.writeBytes.toBytes();
|
||||
}
|
||||
}
|
||||
|
||||
// Format values similar to docker stats
|
||||
const formatBytes = (bytes: number): string => {
|
||||
if (bytes >= 1024 * 1024 * 1024) {
|
||||
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)}GiB`;
|
||||
}
|
||||
if (bytes >= 1024 * 1024) {
|
||||
return `${(bytes / (1024 * 1024)).toFixed(2)}MiB`;
|
||||
}
|
||||
if (bytes >= 1024) {
|
||||
return `${(bytes / 1024).toFixed(2)}KiB`;
|
||||
}
|
||||
return `${bytes}B`;
|
||||
};
|
||||
|
||||
// Format memory usage similar to docker stats format: "used / total"
|
||||
const memUsedFormatted = `${memUsedGB.toFixed(2)}GiB`;
|
||||
const memTotalFormatted = `${memTotalGB.toFixed(2)}GiB`;
|
||||
const memUsageFormatted = `${memUsedFormatted} / ${memTotalFormatted}`;
|
||||
|
||||
// Format network I/O
|
||||
const netInputMb = netInputBytes / (1024 * 1024);
|
||||
const netOutputMb = netOutputBytes / (1024 * 1024);
|
||||
const netIOFormatted = `${netInputMb.toFixed(2)}MB / ${netOutputMb.toFixed(2)}MB`;
|
||||
|
||||
// Format Block I/O
|
||||
const blockIOFormatted = `${formatBytes(blockReadBytes)} / ${formatBytes(blockWriteBytes)}`;
|
||||
|
||||
// Create a stat object compatible with recordAdvancedStats
|
||||
return {
|
||||
CPUPerc: `${cpuUsage.toFixed(2)}%`,
|
||||
MemPerc: `${memUsedPercent.toFixed(2)}%`,
|
||||
MemUsage: memUsageFormatted,
|
||||
BlockIO: blockIOFormatted,
|
||||
NetIO: netIOFormatted,
|
||||
Container: "dokploy",
|
||||
ID: "host-system",
|
||||
Name: "dokploy",
|
||||
};
|
||||
};
|
||||
|
||||
export const getAdvancedStats = async (appName: string) => {
|
||||
return {
|
||||
cpu: await readStatsFile(appName, "cpu"),
|
||||
|
||||
@@ -110,7 +110,8 @@ export const getDokployUrl = async () => {
|
||||
const admin = await findAdmin();
|
||||
|
||||
if (admin.user.host) {
|
||||
return `https://${admin.user.host}`;
|
||||
const protocol = admin.user.https ? "https" : "http";
|
||||
return `${protocol}://${admin.user.host}`;
|
||||
}
|
||||
return `http://${admin.user.serverIp}:${process.env.PORT}`;
|
||||
};
|
||||
|
||||
@@ -7,45 +7,32 @@ import {
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { getAdvancedStats } from "@dokploy/server/monitoring/utils";
|
||||
import {
|
||||
buildApplication,
|
||||
getBuildCommand,
|
||||
mechanizeDockerContainer,
|
||||
} from "@dokploy/server/utils/builders";
|
||||
import { sendBuildErrorNotifications } from "@dokploy/server/utils/notifications/build-error";
|
||||
import { sendBuildSuccessNotifications } from "@dokploy/server/utils/notifications/build-success";
|
||||
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
|
||||
import {
|
||||
cloneBitbucketRepository,
|
||||
getBitbucketCloneCommand,
|
||||
} from "@dokploy/server/utils/providers/bitbucket";
|
||||
import {
|
||||
buildDocker,
|
||||
buildRemoteDocker,
|
||||
} from "@dokploy/server/utils/providers/docker";
|
||||
execAsync,
|
||||
execAsyncRemote,
|
||||
} from "@dokploy/server/utils/process/execAsync";
|
||||
import { cloneBitbucketRepository } from "@dokploy/server/utils/providers/bitbucket";
|
||||
import { buildRemoteDocker } from "@dokploy/server/utils/providers/docker";
|
||||
import {
|
||||
cloneGitRepository,
|
||||
getCustomGitCloneCommand,
|
||||
getGitCommitInfo,
|
||||
} from "@dokploy/server/utils/providers/git";
|
||||
import {
|
||||
cloneGiteaRepository,
|
||||
getGiteaCloneCommand,
|
||||
} from "@dokploy/server/utils/providers/gitea";
|
||||
import {
|
||||
cloneGithubRepository,
|
||||
getGithubCloneCommand,
|
||||
} from "@dokploy/server/utils/providers/github";
|
||||
import {
|
||||
cloneGitlabRepository,
|
||||
getGitlabCloneCommand,
|
||||
} from "@dokploy/server/utils/providers/gitlab";
|
||||
import { cloneGiteaRepository } from "@dokploy/server/utils/providers/gitea";
|
||||
import { cloneGithubRepository } from "@dokploy/server/utils/providers/github";
|
||||
import { cloneGitlabRepository } from "@dokploy/server/utils/providers/gitlab";
|
||||
import { createTraefikConfig } from "@dokploy/server/utils/traefik/application";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { encodeBase64 } from "../utils/docker/utils";
|
||||
import { getDokployUrl } from "./admin";
|
||||
import {
|
||||
createDeployment,
|
||||
createDeploymentPreview,
|
||||
updateDeployment,
|
||||
updateDeploymentStatus,
|
||||
} from "./deployment";
|
||||
import { type Domain, getDomainHost } from "./domain";
|
||||
@@ -192,30 +179,31 @@ export const deployApplication = async ({
|
||||
});
|
||||
|
||||
try {
|
||||
let command = "set -e;";
|
||||
if (application.sourceType === "github") {
|
||||
await cloneGithubRepository({
|
||||
...application,
|
||||
logPath: deployment.logPath,
|
||||
});
|
||||
await buildApplication(application, deployment.logPath);
|
||||
command += await cloneGithubRepository(application);
|
||||
} else if (application.sourceType === "gitlab") {
|
||||
await cloneGitlabRepository(application, deployment.logPath);
|
||||
await buildApplication(application, deployment.logPath);
|
||||
command += await cloneGitlabRepository(application);
|
||||
} else if (application.sourceType === "gitea") {
|
||||
await cloneGiteaRepository(application, deployment.logPath);
|
||||
await buildApplication(application, deployment.logPath);
|
||||
command += await cloneGiteaRepository(application);
|
||||
} else if (application.sourceType === "bitbucket") {
|
||||
await cloneBitbucketRepository(application, deployment.logPath);
|
||||
await buildApplication(application, deployment.logPath);
|
||||
} else if (application.sourceType === "docker") {
|
||||
await buildDocker(application, deployment.logPath);
|
||||
command += await cloneBitbucketRepository(application);
|
||||
} else if (application.sourceType === "git") {
|
||||
await cloneGitRepository(application, deployment.logPath);
|
||||
await buildApplication(application, deployment.logPath);
|
||||
} else if (application.sourceType === "drop") {
|
||||
await buildApplication(application, deployment.logPath);
|
||||
command += await cloneGitRepository(application);
|
||||
} else if (application.sourceType === "docker") {
|
||||
command += await buildRemoteDocker(application);
|
||||
}
|
||||
|
||||
command += getBuildCommand(application);
|
||||
|
||||
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
|
||||
if (application.serverId) {
|
||||
await execAsyncRemote(application.serverId, commandWithLog);
|
||||
} else {
|
||||
await execAsync(commandWithLog);
|
||||
}
|
||||
|
||||
await mechanizeDockerContainer(application);
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updateApplicationStatus(applicationId, "done");
|
||||
|
||||
@@ -239,6 +227,12 @@ export const deployApplication = async ({
|
||||
domains: application.domains,
|
||||
});
|
||||
} catch (error) {
|
||||
const command = `echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath};`;
|
||||
if (application.serverId) {
|
||||
await execAsyncRemote(application.serverId, command);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
await updateApplicationStatus(applicationId, "error");
|
||||
|
||||
@@ -253,8 +247,19 @@ export const deployApplication = async ({
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
} finally {
|
||||
// Only extract commit info for non-docker sources
|
||||
if (application.sourceType !== "docker") {
|
||||
const commitInfo = await getGitCommitInfo(application);
|
||||
|
||||
if (commitInfo) {
|
||||
await updateDeployment(deployment.deploymentId, {
|
||||
title: commitInfo.message,
|
||||
description: `Commit: ${commitInfo.hash}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -276,129 +281,21 @@ export const rebuildApplication = async ({
|
||||
});
|
||||
|
||||
try {
|
||||
if (application.sourceType === "github") {
|
||||
await buildApplication(application, deployment.logPath);
|
||||
} else if (application.sourceType === "gitlab") {
|
||||
await buildApplication(application, deployment.logPath);
|
||||
} else if (application.sourceType === "bitbucket") {
|
||||
await buildApplication(application, deployment.logPath);
|
||||
} else if (application.sourceType === "docker") {
|
||||
await buildDocker(application, deployment.logPath);
|
||||
} else if (application.sourceType === "git") {
|
||||
await buildApplication(application, deployment.logPath);
|
||||
} else if (application.sourceType === "drop") {
|
||||
await buildApplication(application, deployment.logPath);
|
||||
}
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updateApplicationStatus(applicationId, "done");
|
||||
} catch (error) {
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
await updateApplicationStatus(applicationId, "error");
|
||||
throw error;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const deployRemoteApplication = async ({
|
||||
applicationId,
|
||||
titleLog = "Manual deployment",
|
||||
descriptionLog = "",
|
||||
}: {
|
||||
applicationId: string;
|
||||
titleLog: string;
|
||||
descriptionLog: string;
|
||||
}) => {
|
||||
const application = await findApplicationById(applicationId);
|
||||
|
||||
const buildLink = `${await getDokployUrl()}/dashboard/project/${application.environment.projectId}/environment/${application.environmentId}/services/application/${application.applicationId}?tab=deployments`;
|
||||
const deployment = await createDeployment({
|
||||
applicationId: applicationId,
|
||||
title: titleLog,
|
||||
description: descriptionLog,
|
||||
});
|
||||
|
||||
try {
|
||||
let command = "set -e;";
|
||||
// Check case for docker only
|
||||
command += getBuildCommand(application);
|
||||
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
|
||||
if (application.serverId) {
|
||||
let command = "set -e;";
|
||||
if (application.sourceType === "github") {
|
||||
command += await getGithubCloneCommand({
|
||||
...application,
|
||||
serverId: application.serverId,
|
||||
logPath: deployment.logPath,
|
||||
});
|
||||
} else if (application.sourceType === "gitlab") {
|
||||
command += await getGitlabCloneCommand(application, deployment.logPath);
|
||||
} else if (application.sourceType === "bitbucket") {
|
||||
command += await getBitbucketCloneCommand(
|
||||
application,
|
||||
deployment.logPath,
|
||||
);
|
||||
} else if (application.sourceType === "gitea") {
|
||||
command += await getGiteaCloneCommand(application, deployment.logPath);
|
||||
} else if (application.sourceType === "git") {
|
||||
command += await getCustomGitCloneCommand(
|
||||
application,
|
||||
deployment.logPath,
|
||||
);
|
||||
} else if (application.sourceType === "docker") {
|
||||
command += await buildRemoteDocker(application, deployment.logPath);
|
||||
}
|
||||
|
||||
if (application.sourceType !== "docker") {
|
||||
command += getBuildCommand(application, deployment.logPath);
|
||||
}
|
||||
await execAsyncRemote(application.serverId, command);
|
||||
await mechanizeDockerContainer(application);
|
||||
await execAsyncRemote(application.serverId, commandWithLog);
|
||||
} else {
|
||||
await execAsync(commandWithLog);
|
||||
}
|
||||
|
||||
await mechanizeDockerContainer(application);
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updateApplicationStatus(applicationId, "done");
|
||||
|
||||
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,
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
|
||||
const encodedContent = encodeBase64(errorMessage);
|
||||
|
||||
await execAsyncRemote(
|
||||
application.serverId,
|
||||
`
|
||||
echo "\n\n===================================EXTRA LOGS============================================" >> ${deployment.logPath};
|
||||
echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath};
|
||||
echo "${encodedContent}" | base64 -d >> "${deployment.logPath}";`,
|
||||
);
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
await updateApplicationStatus(applicationId, "error");
|
||||
|
||||
await sendBuildErrorNotifications({
|
||||
projectName: application.environment.project.name,
|
||||
applicationName: application.name,
|
||||
applicationType: "application",
|
||||
errorMessage: `Please check the logs for details: ${errorMessage}`,
|
||||
buildLink,
|
||||
organizationId: application.environment.project.organizationId,
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -475,14 +372,22 @@ export const deployPreviewApplication = async ({
|
||||
application.buildArgs = `${application.previewBuildArgs}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
||||
application.buildSecrets = `${application.previewBuildSecrets}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
||||
|
||||
let command = "set -e;";
|
||||
if (application.sourceType === "github") {
|
||||
await cloneGithubRepository({
|
||||
command += await cloneGithubRepository({
|
||||
...application,
|
||||
appName: previewDeployment.appName,
|
||||
branch: previewDeployment.branch,
|
||||
logPath: deployment.logPath,
|
||||
});
|
||||
await buildApplication(application, deployment.logPath);
|
||||
command += getBuildCommand(application);
|
||||
|
||||
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
|
||||
if (application.serverId) {
|
||||
await execAsyncRemote(application.serverId, commandWithLog);
|
||||
} else {
|
||||
await execAsync(commandWithLog);
|
||||
}
|
||||
await mechanizeDockerContainer(application);
|
||||
}
|
||||
const successComment = getIssueComment(
|
||||
application.name,
|
||||
@@ -513,170 +418,10 @@ export const deployPreviewApplication = async ({
|
||||
return true;
|
||||
};
|
||||
|
||||
export const deployRemotePreviewApplication = async ({
|
||||
applicationId,
|
||||
titleLog = "Preview Deployment",
|
||||
descriptionLog = "",
|
||||
previewDeploymentId,
|
||||
}: {
|
||||
applicationId: string;
|
||||
titleLog: string;
|
||||
descriptionLog: string;
|
||||
previewDeploymentId: string;
|
||||
}) => {
|
||||
const application = await findApplicationById(applicationId);
|
||||
|
||||
const deployment = await createDeploymentPreview({
|
||||
title: titleLog,
|
||||
description: descriptionLog,
|
||||
previewDeploymentId: previewDeploymentId,
|
||||
});
|
||||
|
||||
const previewDeployment =
|
||||
await findPreviewDeploymentById(previewDeploymentId);
|
||||
|
||||
await updatePreviewDeployment(previewDeploymentId, {
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const previewDomain = getDomainHost(previewDeployment?.domain as Domain);
|
||||
const issueParams = {
|
||||
owner: application?.owner || "",
|
||||
repository: application?.repository || "",
|
||||
issue_number: previewDeployment.pullRequestNumber,
|
||||
comment_id: Number.parseInt(previewDeployment.pullRequestCommentId),
|
||||
githubId: application?.githubId || "",
|
||||
};
|
||||
try {
|
||||
const commentExists = await issueCommentExists({
|
||||
...issueParams,
|
||||
});
|
||||
if (!commentExists) {
|
||||
const result = await createPreviewDeploymentComment({
|
||||
...issueParams,
|
||||
previewDomain,
|
||||
appName: previewDeployment.appName,
|
||||
githubId: application?.githubId || "",
|
||||
previewDeploymentId,
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Pull request comment not found",
|
||||
});
|
||||
}
|
||||
|
||||
issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId);
|
||||
}
|
||||
const buildingComment = getIssueComment(
|
||||
application.name,
|
||||
"running",
|
||||
previewDomain,
|
||||
);
|
||||
await updateIssueComment({
|
||||
...issueParams,
|
||||
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
|
||||
});
|
||||
application.appName = previewDeployment.appName;
|
||||
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
||||
application.buildArgs = `${application.previewBuildArgs}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
||||
application.buildSecrets = `${application.previewBuildSecrets}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
||||
|
||||
if (application.serverId) {
|
||||
let command = "set -e;";
|
||||
if (application.sourceType === "github") {
|
||||
command += await getGithubCloneCommand({
|
||||
...application,
|
||||
appName: previewDeployment.appName,
|
||||
branch: previewDeployment.branch,
|
||||
serverId: application.serverId,
|
||||
logPath: deployment.logPath,
|
||||
});
|
||||
}
|
||||
|
||||
command += getBuildCommand(application, deployment.logPath);
|
||||
await execAsyncRemote(application.serverId, command);
|
||||
await mechanizeDockerContainer(application);
|
||||
}
|
||||
|
||||
const successComment = getIssueComment(
|
||||
application.name,
|
||||
"success",
|
||||
previewDomain,
|
||||
);
|
||||
await updateIssueComment({
|
||||
...issueParams,
|
||||
body: `### Dokploy Preview Deployment\n\n${successComment}`,
|
||||
});
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updatePreviewDeployment(previewDeploymentId, {
|
||||
previewStatus: "done",
|
||||
});
|
||||
} catch (error) {
|
||||
const comment = getIssueComment(application.name, "error", previewDomain);
|
||||
await updateIssueComment({
|
||||
...issueParams,
|
||||
body: `### Dokploy Preview Deployment\n\n${comment}`,
|
||||
});
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
await updatePreviewDeployment(previewDeploymentId, {
|
||||
previewStatus: "error",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const rebuildRemoteApplication = async ({
|
||||
applicationId,
|
||||
titleLog = "Rebuild deployment",
|
||||
descriptionLog = "",
|
||||
}: {
|
||||
applicationId: string;
|
||||
titleLog: string;
|
||||
descriptionLog: string;
|
||||
}) => {
|
||||
const application = await findApplicationById(applicationId);
|
||||
|
||||
const deployment = await createDeployment({
|
||||
applicationId: applicationId,
|
||||
title: titleLog,
|
||||
description: descriptionLog,
|
||||
});
|
||||
|
||||
try {
|
||||
if (application.serverId) {
|
||||
if (application.sourceType !== "docker") {
|
||||
let command = "set -e;";
|
||||
command += getBuildCommand(application, deployment.logPath);
|
||||
await execAsyncRemote(application.serverId, command);
|
||||
}
|
||||
await mechanizeDockerContainer(application);
|
||||
}
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updateApplicationStatus(applicationId, "done");
|
||||
} catch (error) {
|
||||
// @ts-ignore
|
||||
const encodedContent = encodeBase64(error?.message);
|
||||
|
||||
await execAsyncRemote(
|
||||
application.serverId,
|
||||
`
|
||||
echo "\n\n===================================EXTRA LOGS============================================" >> ${deployment.logPath};
|
||||
echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath};
|
||||
echo "${encodedContent}" | base64 -d >> "${deployment.logPath}";`,
|
||||
);
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
await updateApplicationStatus(applicationId, "error");
|
||||
throw error;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const getApplicationStats = async (appName: string) => {
|
||||
if (appName === "dokploy") {
|
||||
return await getAdvancedStats(appName);
|
||||
}
|
||||
const filter = {
|
||||
status: ["running"],
|
||||
label: [`com.docker.swarm.service.name=${appName}`],
|
||||
|
||||
@@ -7,14 +7,10 @@ import {
|
||||
cleanAppName,
|
||||
compose,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import {
|
||||
buildCompose,
|
||||
getBuildComposeCommand,
|
||||
} from "@dokploy/server/utils/builders/compose";
|
||||
import { getBuildComposeCommand } from "@dokploy/server/utils/builders/compose";
|
||||
import { randomizeSpecificationFile } from "@dokploy/server/utils/docker/compose";
|
||||
import {
|
||||
cloneCompose,
|
||||
cloneComposeRemote,
|
||||
loadDockerCompose,
|
||||
loadDockerComposeRemote,
|
||||
} from "@dokploy/server/utils/docker/domain";
|
||||
@@ -25,35 +21,23 @@ import {
|
||||
execAsync,
|
||||
execAsyncRemote,
|
||||
} from "@dokploy/server/utils/process/execAsync";
|
||||
import {
|
||||
cloneBitbucketRepository,
|
||||
getBitbucketCloneCommand,
|
||||
} from "@dokploy/server/utils/providers/bitbucket";
|
||||
import { cloneBitbucketRepository } from "@dokploy/server/utils/providers/bitbucket";
|
||||
import {
|
||||
cloneGitRepository,
|
||||
getCustomGitCloneCommand,
|
||||
getGitCommitInfo,
|
||||
} from "@dokploy/server/utils/providers/git";
|
||||
import {
|
||||
cloneGiteaRepository,
|
||||
getGiteaCloneCommand,
|
||||
} from "@dokploy/server/utils/providers/gitea";
|
||||
import {
|
||||
cloneGithubRepository,
|
||||
getGithubCloneCommand,
|
||||
} from "@dokploy/server/utils/providers/github";
|
||||
import {
|
||||
cloneGitlabRepository,
|
||||
getGitlabCloneCommand,
|
||||
} from "@dokploy/server/utils/providers/gitlab";
|
||||
import {
|
||||
createComposeFile,
|
||||
getCreateComposeFileCommand,
|
||||
} from "@dokploy/server/utils/providers/raw";
|
||||
import { cloneGiteaRepository } from "@dokploy/server/utils/providers/gitea";
|
||||
import { cloneGithubRepository } from "@dokploy/server/utils/providers/github";
|
||||
import { cloneGitlabRepository } from "@dokploy/server/utils/providers/gitlab";
|
||||
import { getCreateComposeFileCommand } from "@dokploy/server/utils/providers/raw";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { encodeBase64 } from "../utils/docker/utils";
|
||||
import { getDokployUrl } from "./admin";
|
||||
import { createDeploymentCompose, updateDeploymentStatus } from "./deployment";
|
||||
import {
|
||||
createDeploymentCompose,
|
||||
updateDeployment,
|
||||
updateDeploymentStatus,
|
||||
} from "./deployment";
|
||||
import { validUniqueServerAppName } from "./project";
|
||||
|
||||
export type Compose = typeof compose.$inferSelect;
|
||||
@@ -163,10 +147,11 @@ export const loadServices = async (
|
||||
const compose = await findComposeById(composeId);
|
||||
|
||||
if (type === "fetch") {
|
||||
const command = await cloneCompose(compose);
|
||||
if (compose.serverId) {
|
||||
await cloneComposeRemote(compose);
|
||||
await execAsyncRemote(compose.serverId, command);
|
||||
} else {
|
||||
await cloneCompose(compose);
|
||||
await execAsync(command);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,24 +220,41 @@ export const deployCompose = async ({
|
||||
});
|
||||
|
||||
try {
|
||||
const entity = {
|
||||
...compose,
|
||||
type: "compose" as const,
|
||||
};
|
||||
let command = "set -e;";
|
||||
if (compose.sourceType === "github") {
|
||||
await cloneGithubRepository({
|
||||
...compose,
|
||||
logPath: deployment.logPath,
|
||||
type: "compose",
|
||||
});
|
||||
command += await cloneGithubRepository(entity);
|
||||
} else if (compose.sourceType === "gitlab") {
|
||||
await cloneGitlabRepository(compose, deployment.logPath, true);
|
||||
command += await cloneGitlabRepository(entity);
|
||||
} else if (compose.sourceType === "bitbucket") {
|
||||
await cloneBitbucketRepository(compose, deployment.logPath, true);
|
||||
command += await cloneBitbucketRepository(entity);
|
||||
} else if (compose.sourceType === "git") {
|
||||
await cloneGitRepository(compose, deployment.logPath, true);
|
||||
command += await cloneGitRepository(entity);
|
||||
} else if (compose.sourceType === "gitea") {
|
||||
await cloneGiteaRepository(compose, deployment.logPath, true);
|
||||
command += await cloneGiteaRepository(entity);
|
||||
} else if (compose.sourceType === "raw") {
|
||||
await createComposeFile(compose, deployment.logPath);
|
||||
command += getCreateComposeFileCommand(entity);
|
||||
}
|
||||
await buildCompose(compose, deployment.logPath);
|
||||
|
||||
let commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
|
||||
if (compose.serverId) {
|
||||
await execAsyncRemote(compose.serverId, commandWithLog);
|
||||
} else {
|
||||
await execAsync(commandWithLog);
|
||||
}
|
||||
|
||||
command = "set -e;";
|
||||
command += await getBuildComposeCommand(entity);
|
||||
commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
|
||||
if (compose.serverId) {
|
||||
await execAsyncRemote(compose.serverId, commandWithLog);
|
||||
} else {
|
||||
await execAsync(commandWithLog);
|
||||
}
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updateCompose(composeId, {
|
||||
composeStatus: "done",
|
||||
@@ -281,6 +283,19 @@ export const deployCompose = async ({
|
||||
organizationId: compose.environment.project.organizationId,
|
||||
});
|
||||
throw error;
|
||||
} finally {
|
||||
if (compose.sourceType !== "raw") {
|
||||
const commitInfo = await getGitCommitInfo({
|
||||
...compose,
|
||||
type: "compose",
|
||||
});
|
||||
if (commitInfo) {
|
||||
await updateDeployment(deployment.deploymentId, {
|
||||
title: commitInfo.message,
|
||||
description: `Commit: ${commitInfo.hash}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -302,154 +317,23 @@ export const rebuildCompose = async ({
|
||||
});
|
||||
|
||||
try {
|
||||
let command = "set -e;";
|
||||
if (compose.sourceType === "raw") {
|
||||
await createComposeFile(compose, deployment.logPath);
|
||||
command += getCreateComposeFileCommand(compose);
|
||||
}
|
||||
await buildCompose(compose, deployment.logPath);
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updateCompose(composeId, {
|
||||
composeStatus: "done",
|
||||
});
|
||||
} catch (error) {
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
await updateCompose(composeId, {
|
||||
composeStatus: "error",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const deployRemoteCompose = async ({
|
||||
composeId,
|
||||
titleLog = "Manual deployment",
|
||||
descriptionLog = "",
|
||||
}: {
|
||||
composeId: string;
|
||||
titleLog: string;
|
||||
descriptionLog: string;
|
||||
}) => {
|
||||
const compose = await findComposeById(composeId);
|
||||
|
||||
const buildLink = `${await getDokployUrl()}/dashboard/project/${
|
||||
compose.environment.projectId
|
||||
}/environment/${compose.environmentId}/services/compose/${compose.composeId}?tab=deployments`;
|
||||
const deployment = await createDeploymentCompose({
|
||||
composeId: composeId,
|
||||
title: titleLog,
|
||||
description: descriptionLog,
|
||||
});
|
||||
try {
|
||||
let commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
|
||||
if (compose.serverId) {
|
||||
let command = "set -e;";
|
||||
|
||||
if (compose.sourceType === "github") {
|
||||
command += await getGithubCloneCommand({
|
||||
...compose,
|
||||
logPath: deployment.logPath,
|
||||
type: "compose",
|
||||
serverId: compose.serverId,
|
||||
});
|
||||
} else if (compose.sourceType === "gitlab") {
|
||||
command += await getGitlabCloneCommand(
|
||||
compose,
|
||||
deployment.logPath,
|
||||
true,
|
||||
);
|
||||
} else if (compose.sourceType === "bitbucket") {
|
||||
command += await getBitbucketCloneCommand(
|
||||
compose,
|
||||
deployment.logPath,
|
||||
true,
|
||||
);
|
||||
} else if (compose.sourceType === "git") {
|
||||
command += await getCustomGitCloneCommand(
|
||||
compose,
|
||||
deployment.logPath,
|
||||
true,
|
||||
);
|
||||
console.log(command);
|
||||
} else if (compose.sourceType === "raw") {
|
||||
command += getCreateComposeFileCommand(compose, deployment.logPath);
|
||||
} else if (compose.sourceType === "gitea") {
|
||||
command += await getGiteaCloneCommand(
|
||||
compose,
|
||||
deployment.logPath,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
await execAsyncRemote(compose.serverId, command);
|
||||
await getBuildComposeCommand(compose, deployment.logPath);
|
||||
}
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updateCompose(composeId, {
|
||||
composeStatus: "done",
|
||||
});
|
||||
|
||||
await sendBuildSuccessNotifications({
|
||||
projectName: compose.environment.project.name,
|
||||
applicationName: compose.name,
|
||||
applicationType: "compose",
|
||||
buildLink,
|
||||
organizationId: compose.environment.project.organizationId,
|
||||
domains: compose.domains,
|
||||
});
|
||||
} catch (error) {
|
||||
// @ts-ignore
|
||||
const encodedContent = encodeBase64(error?.message);
|
||||
|
||||
await execAsyncRemote(
|
||||
compose.serverId,
|
||||
`
|
||||
echo "\n\n===================================EXTRA LOGS============================================" >> ${deployment.logPath};
|
||||
echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath};
|
||||
echo "${encodedContent}" | base64 -d >> "${deployment.logPath}";`,
|
||||
);
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
await updateCompose(composeId, {
|
||||
composeStatus: "error",
|
||||
});
|
||||
await sendBuildErrorNotifications({
|
||||
projectName: compose.environment.project.name,
|
||||
applicationName: compose.name,
|
||||
applicationType: "compose",
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error building",
|
||||
buildLink,
|
||||
organizationId: compose.environment.project.organizationId,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const rebuildRemoteCompose = async ({
|
||||
composeId,
|
||||
titleLog = "Rebuild deployment",
|
||||
descriptionLog = "",
|
||||
}: {
|
||||
composeId: string;
|
||||
titleLog: string;
|
||||
descriptionLog: string;
|
||||
}) => {
|
||||
const compose = await findComposeById(composeId);
|
||||
|
||||
const deployment = await createDeploymentCompose({
|
||||
composeId: composeId,
|
||||
title: titleLog,
|
||||
description: descriptionLog,
|
||||
});
|
||||
|
||||
try {
|
||||
if (compose.sourceType === "raw") {
|
||||
const command = getCreateComposeFileCommand(compose, deployment.logPath);
|
||||
await execAsyncRemote(compose.serverId, command);
|
||||
await execAsyncRemote(compose.serverId, commandWithLog);
|
||||
} else {
|
||||
await execAsync(commandWithLog);
|
||||
}
|
||||
command += await getBuildComposeCommand(compose);
|
||||
commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
|
||||
if (compose.serverId) {
|
||||
await getBuildComposeCommand(compose, deployment.logPath);
|
||||
await execAsyncRemote(compose.serverId, commandWithLog);
|
||||
} else {
|
||||
await execAsync(commandWithLog);
|
||||
}
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
@@ -457,16 +341,6 @@ export const rebuildRemoteCompose = async ({
|
||||
composeStatus: "done",
|
||||
});
|
||||
} catch (error) {
|
||||
// @ts-ignore
|
||||
const encodedContent = encodeBase64(error?.message);
|
||||
|
||||
await execAsyncRemote(
|
||||
compose.serverId,
|
||||
`
|
||||
echo "\n\n===================================EXTRA LOGS============================================" >> ${deployment.logPath};
|
||||
echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath};
|
||||
echo "${encodedContent}" | base64 -d >> "${deployment.logPath}";`,
|
||||
);
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
await updateCompose(composeId, {
|
||||
composeStatus: "error",
|
||||
|
||||
@@ -74,20 +74,21 @@ export const createDeployment = async (
|
||||
>,
|
||||
) => {
|
||||
const application = await findApplicationById(deployment.applicationId);
|
||||
|
||||
try {
|
||||
await removeLastTenDeployments(
|
||||
deployment.applicationId,
|
||||
"application",
|
||||
application.serverId,
|
||||
);
|
||||
const { LOGS_PATH } = paths(!!application.serverId);
|
||||
const serverId = application.serverId;
|
||||
|
||||
const { LOGS_PATH } = paths(!!serverId);
|
||||
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
||||
const fileName = `${application.appName}-${formattedDateTime}.log`;
|
||||
const logFilePath = path.join(LOGS_PATH, application.appName, fileName);
|
||||
|
||||
if (application.serverId) {
|
||||
const server = await findServerById(application.serverId);
|
||||
if (serverId) {
|
||||
const server = await findServerById(serverId);
|
||||
|
||||
const command = `
|
||||
mkdir -p ${LOGS_PATH}/${application.appName};
|
||||
@@ -99,7 +100,7 @@ export const createDeployment = async (
|
||||
await fsPromises.mkdir(path.join(LOGS_PATH, application.appName), {
|
||||
recursive: true,
|
||||
});
|
||||
await fsPromises.writeFile(logFilePath, "Initializing deployment");
|
||||
await fsPromises.writeFile(logFilePath, "Initializing deployment\n");
|
||||
}
|
||||
|
||||
const deploymentCreate = await db
|
||||
@@ -249,7 +250,7 @@ export const createDeploymentCompose = async (
|
||||
|
||||
const command = `
|
||||
mkdir -p ${LOGS_PATH}/${compose.appName};
|
||||
echo "Initializing deployment" >> ${logFilePath};
|
||||
echo "Initializing deployment\n" >> ${logFilePath};
|
||||
`;
|
||||
|
||||
await execAsyncRemote(server.serverId, command);
|
||||
@@ -257,7 +258,7 @@ echo "Initializing deployment" >> ${logFilePath};
|
||||
await fsPromises.mkdir(path.join(LOGS_PATH, compose.appName), {
|
||||
recursive: true,
|
||||
});
|
||||
await fsPromises.writeFile(logFilePath, "Initializing deployment");
|
||||
await fsPromises.writeFile(logFilePath, "Initializing deployment\n");
|
||||
}
|
||||
|
||||
const deploymentCreate = await db
|
||||
|
||||
@@ -34,13 +34,21 @@ export const findEnvironmentById = async (environmentId: string) => {
|
||||
const environment = await db.query.environments.findFirst({
|
||||
where: eq(environments.environmentId, environmentId),
|
||||
with: {
|
||||
applications: true,
|
||||
applications: {
|
||||
with: {
|
||||
deployments: true,
|
||||
},
|
||||
},
|
||||
mariadb: true,
|
||||
mongo: true,
|
||||
mysql: true,
|
||||
postgres: true,
|
||||
redis: true,
|
||||
compose: true,
|
||||
compose: {
|
||||
with: {
|
||||
deployments: true,
|
||||
},
|
||||
},
|
||||
project: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@ export const TRAEFIK_PORT =
|
||||
Number.parseInt(process.env.TRAEFIK_PORT!, 10) || 80;
|
||||
export const TRAEFIK_HTTP3_PORT =
|
||||
Number.parseInt(process.env.TRAEFIK_HTTP3_PORT!, 10) || 443;
|
||||
export const TRAEFIK_VERSION = process.env.TRAEFIK_VERSION || "3.5.0";
|
||||
export const TRAEFIK_VERSION = process.env.TRAEFIK_VERSION || "3.6.1";
|
||||
|
||||
export interface TraefikOptions {
|
||||
env?: string[];
|
||||
|
||||
@@ -1,117 +1,28 @@
|
||||
import {
|
||||
createWriteStream,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import type { InferResultType } from "@dokploy/server/types/with";
|
||||
import boxen from "boxen";
|
||||
import {
|
||||
writeDomainsToCompose,
|
||||
writeDomainsToComposeRemote,
|
||||
} from "../docker/domain";
|
||||
import { writeDomainsToComposeRemote } from "../docker/domain";
|
||||
import {
|
||||
encodeBase64,
|
||||
getEnviromentVariablesObject,
|
||||
prepareEnvironmentVariables,
|
||||
} from "../docker/utils";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
|
||||
export type ComposeNested = InferResultType<
|
||||
"compose",
|
||||
{ environment: { with: { project: true } }; mounts: true; domains: true }
|
||||
>;
|
||||
export const buildCompose = async (compose: ComposeNested, logPath: string) => {
|
||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||
const { sourceType, appName, mounts, composeType, domains } = compose;
|
||||
try {
|
||||
const { COMPOSE_PATH } = paths();
|
||||
const command = createCommand(compose);
|
||||
await writeDomainsToCompose(compose, domains);
|
||||
createEnvFile(compose);
|
||||
|
||||
if (compose.isolatedDeployment) {
|
||||
await execAsync(
|
||||
`docker network inspect ${compose.appName} >/dev/null 2>&1 || docker network create ${composeType === "stack" ? "--driver overlay" : ""} --attachable ${compose.appName}`,
|
||||
);
|
||||
}
|
||||
|
||||
const logContent = `
|
||||
App Name: ${appName}
|
||||
Build Compose 🐳
|
||||
Detected: ${mounts.length} mounts 📂
|
||||
Command: docker ${command}
|
||||
Source Type: docker ${sourceType} ✅
|
||||
Compose Type: ${composeType} ✅`;
|
||||
const logBox = boxen(logContent, {
|
||||
padding: {
|
||||
left: 1,
|
||||
right: 1,
|
||||
bottom: 1,
|
||||
},
|
||||
width: 80,
|
||||
borderStyle: "double",
|
||||
});
|
||||
writeStream.write(`\n${logBox}\n`);
|
||||
const projectPath = join(COMPOSE_PATH, compose.appName, "code");
|
||||
|
||||
await spawnAsync(
|
||||
"docker",
|
||||
[...command.split(" ")],
|
||||
(data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data.toString());
|
||||
}
|
||||
},
|
||||
{
|
||||
cwd: projectPath,
|
||||
env: {
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
PATH: process.env.PATH,
|
||||
...(composeType === "stack" && {
|
||||
...getEnviromentVariablesObject(
|
||||
compose.env,
|
||||
compose.environment.project.env,
|
||||
),
|
||||
}),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (compose.isolatedDeployment) {
|
||||
await execAsync(
|
||||
`docker network connect ${compose.appName} $(docker ps --filter "name=dokploy-traefik" -q) >/dev/null 2>&1`,
|
||||
).catch(() => {});
|
||||
}
|
||||
|
||||
writeStream.write("Docker Compose Deployed: ✅");
|
||||
} catch (error) {
|
||||
writeStream.write(`Error ❌ ${(error as Error).message}`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
}
|
||||
};
|
||||
|
||||
export const getBuildComposeCommand = async (
|
||||
compose: ComposeNested,
|
||||
logPath: string,
|
||||
) => {
|
||||
const { COMPOSE_PATH } = paths(true);
|
||||
export const getBuildComposeCommand = async (compose: ComposeNested) => {
|
||||
const { COMPOSE_PATH } = paths(!!compose.serverId);
|
||||
const { sourceType, appName, mounts, composeType, domains } = compose;
|
||||
const command = createCommand(compose);
|
||||
const envCommand = getCreateEnvFileCommand(compose);
|
||||
const projectPath = join(COMPOSE_PATH, compose.appName, "code");
|
||||
const exportEnvCommand = getExportEnvCommand(compose);
|
||||
|
||||
const newCompose = await writeDomainsToComposeRemote(
|
||||
compose,
|
||||
domains,
|
||||
logPath,
|
||||
);
|
||||
const newCompose = await writeDomainsToComposeRemote(compose, domains);
|
||||
const logContent = `
|
||||
App Name: ${appName}
|
||||
Build Compose 🐳
|
||||
@@ -133,7 +44,7 @@ Compose Type: ${composeType} ✅`;
|
||||
const bashCommand = `
|
||||
set -e
|
||||
{
|
||||
echo "${logBox}" >> "${logPath}"
|
||||
echo "${logBox}";
|
||||
|
||||
${newCompose}
|
||||
|
||||
@@ -143,17 +54,18 @@ Compose Type: ${composeType} ✅`;
|
||||
|
||||
${exportEnvCommand}
|
||||
${compose.isolatedDeployment ? `docker network inspect ${compose.appName} >/dev/null 2>&1 || docker network create --attachable ${compose.appName}` : ""}
|
||||
docker ${command.split(" ").join(" ")} >> "${logPath}" 2>&1 || { echo "Error: ❌ Docker command failed" >> "${logPath}"; exit 1; }
|
||||
docker ${command.split(" ").join(" ")} 2>&1 || { echo "Error: ❌ Docker command failed"; exit 1; }
|
||||
${compose.isolatedDeployment ? `docker network connect ${compose.appName} $(docker ps --filter "name=dokploy-traefik" -q) >/dev/null 2>&1` : ""}
|
||||
|
||||
echo "Docker Compose Deployed: ✅" >> "${logPath}"
|
||||
echo "Docker Compose Deployed: ✅";
|
||||
} || {
|
||||
echo "Error: ❌ Script execution failed" >> "${logPath}"
|
||||
echo "Error: ❌ Script execution failed";
|
||||
exit 1
|
||||
}
|
||||
`;
|
||||
|
||||
return await execAsyncRemote(compose.serverId, bashCommand);
|
||||
return bashCommand;
|
||||
// return await execAsyncRemote(compose.serverId, bashCommand);
|
||||
};
|
||||
|
||||
const sanitizeCommand = (command: string) => {
|
||||
@@ -185,38 +97,8 @@ export const createCommand = (compose: ComposeNested) => {
|
||||
return command;
|
||||
};
|
||||
|
||||
const createEnvFile = (compose: ComposeNested) => {
|
||||
const { COMPOSE_PATH } = paths();
|
||||
const { env, composePath, appName } = compose;
|
||||
const composeFilePath =
|
||||
join(COMPOSE_PATH, appName, "code", composePath) ||
|
||||
join(COMPOSE_PATH, appName, "code", "docker-compose.yml");
|
||||
|
||||
const envFilePath = join(dirname(composeFilePath), ".env");
|
||||
let envContent = `APP_NAME=${appName}\n`;
|
||||
envContent += env || "";
|
||||
if (!envContent.includes("DOCKER_CONFIG")) {
|
||||
envContent += "\nDOCKER_CONFIG=/root/.docker";
|
||||
}
|
||||
|
||||
if (compose.randomize) {
|
||||
envContent += `\nCOMPOSE_PREFIX=${compose.suffix}`;
|
||||
}
|
||||
|
||||
const envFileContent = prepareEnvironmentVariables(
|
||||
envContent,
|
||||
compose.environment.project.env,
|
||||
compose.environment.env,
|
||||
).join("\n");
|
||||
|
||||
if (!existsSync(dirname(envFilePath))) {
|
||||
mkdirSync(dirname(envFilePath), { recursive: true });
|
||||
}
|
||||
writeFileSync(envFilePath, envFileContent);
|
||||
};
|
||||
|
||||
export const getCreateEnvFileCommand = (compose: ComposeNested) => {
|
||||
const { COMPOSE_PATH } = paths(true);
|
||||
const { COMPOSE_PATH } = paths(!!compose.serverId);
|
||||
const { env, composePath, appName } = compose;
|
||||
const composeFilePath =
|
||||
join(COMPOSE_PATH, appName, "code", composePath) ||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { WriteStream } from "node:fs";
|
||||
import {
|
||||
getEnviromentVariablesObject,
|
||||
prepareEnvironmentVariables,
|
||||
@@ -7,103 +6,10 @@ import {
|
||||
getBuildAppDirectory,
|
||||
getDockerContextPath,
|
||||
} from "../filesystem/directory";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
import type { ApplicationNested } from ".";
|
||||
import { createEnvFile, createEnvFileCommand } from "./utils";
|
||||
import { createEnvFileCommand } from "./utils";
|
||||
|
||||
export const buildCustomDocker = async (
|
||||
application: ApplicationNested,
|
||||
writeStream: WriteStream,
|
||||
) => {
|
||||
const {
|
||||
appName,
|
||||
env,
|
||||
publishDirectory,
|
||||
buildArgs,
|
||||
buildSecrets,
|
||||
dockerBuildStage,
|
||||
cleanCache,
|
||||
} = application;
|
||||
const dockerFilePath = getBuildAppDirectory(application);
|
||||
try {
|
||||
const image = `${appName}`;
|
||||
|
||||
const defaultContextPath =
|
||||
dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || ".";
|
||||
|
||||
const dockerContextPath = getDockerContextPath(application);
|
||||
|
||||
const commandArgs = ["build", "-t", image, "-f", dockerFilePath, "."];
|
||||
|
||||
if (cleanCache) {
|
||||
commandArgs.push("--no-cache");
|
||||
}
|
||||
|
||||
if (dockerBuildStage) {
|
||||
commandArgs.push("--target", dockerBuildStage);
|
||||
}
|
||||
|
||||
const args = prepareEnvironmentVariables(
|
||||
buildArgs,
|
||||
application.environment.project.env,
|
||||
application.environment.env,
|
||||
);
|
||||
|
||||
for (const arg of args) {
|
||||
commandArgs.push("--build-arg", arg);
|
||||
}
|
||||
|
||||
const secrets = getEnviromentVariablesObject(
|
||||
buildSecrets,
|
||||
application.environment.project.env,
|
||||
application.environment.env,
|
||||
);
|
||||
|
||||
for (const key in secrets) {
|
||||
// Although buildx is smart enough to know we may be referring to an environment variable name,
|
||||
// we still make sure it doesn't fall back to type=file.
|
||||
// See: https://docs.docker.com/reference/cli/docker/buildx/build/#secret
|
||||
commandArgs.push("--secret", `type=env,id=${key}`);
|
||||
}
|
||||
|
||||
/*
|
||||
Do not generate an environment file when publishDirectory is specified,
|
||||
as it could be publicly exposed.
|
||||
*/
|
||||
if (!publishDirectory) {
|
||||
createEnvFile(
|
||||
dockerFilePath,
|
||||
env,
|
||||
application.environment.project.env,
|
||||
application.environment.env,
|
||||
);
|
||||
}
|
||||
|
||||
await spawnAsync(
|
||||
"docker",
|
||||
commandArgs,
|
||||
(data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
},
|
||||
{
|
||||
cwd: dockerContextPath || defaultContextPath,
|
||||
env: {
|
||||
...process.env,
|
||||
...secrets,
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getDockerCommand = (
|
||||
application: ApplicationNested,
|
||||
logPath: string,
|
||||
) => {
|
||||
export const getDockerCommand = (application: ApplicationNested) => {
|
||||
const {
|
||||
appName,
|
||||
env,
|
||||
@@ -176,17 +82,17 @@ export const getDockerCommand = (
|
||||
}
|
||||
|
||||
command += `
|
||||
echo "Building ${appName}" >> ${logPath};
|
||||
cd ${dockerContextPath} >> ${logPath} 2>> ${logPath} || {
|
||||
echo "❌ The path ${dockerContextPath} does not exist" >> ${logPath};
|
||||
echo "Building ${appName}" ;
|
||||
cd ${dockerContextPath} || {
|
||||
echo "❌ The path ${dockerContextPath} does not exist" ;
|
||||
exit 1;
|
||||
}
|
||||
|
||||
${joinedSecrets} docker ${commandArgs.join(" ")} >> ${logPath} 2>> ${logPath} || {
|
||||
echo "❌ Docker build failed" >> ${logPath};
|
||||
${joinedSecrets} docker ${commandArgs.join(" ")} || {
|
||||
echo "❌ Docker build failed" ;
|
||||
exit 1;
|
||||
}
|
||||
echo "✅ Docker build completed." >> ${logPath};
|
||||
echo "✅ Docker build completed." ;
|
||||
`;
|
||||
|
||||
return command;
|
||||
|
||||
@@ -1,54 +1,8 @@
|
||||
import type { WriteStream } from "node:fs";
|
||||
import { prepareEnvironmentVariables } from "../docker/utils";
|
||||
import { getBuildAppDirectory } from "../filesystem/directory";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
import type { ApplicationNested } from ".";
|
||||
|
||||
// TODO: integrate in the vps sudo chown -R $(whoami) ~/.docker
|
||||
export const buildHeroku = async (
|
||||
application: ApplicationNested,
|
||||
writeStream: WriteStream,
|
||||
) => {
|
||||
const { env, appName, cleanCache } = application;
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
env,
|
||||
application.environment.project.env,
|
||||
application.environment.env,
|
||||
);
|
||||
try {
|
||||
const args = [
|
||||
"build",
|
||||
appName,
|
||||
"--path",
|
||||
buildAppDirectory,
|
||||
"--builder",
|
||||
`heroku/builder:${application.herokuVersion || "24"}`,
|
||||
];
|
||||
|
||||
for (const env of envVariables) {
|
||||
args.push("--env", env);
|
||||
}
|
||||
|
||||
if (cleanCache) {
|
||||
args.push("--clear-cache");
|
||||
}
|
||||
|
||||
await spawnAsync("pack", args, (data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const getHerokuCommand = (
|
||||
application: ApplicationNested,
|
||||
logPath: string,
|
||||
) => {
|
||||
export const getHerokuCommand = (application: ApplicationNested) => {
|
||||
const { env, appName, cleanCache } = application;
|
||||
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
@@ -77,12 +31,12 @@ export const getHerokuCommand = (
|
||||
|
||||
const command = `pack ${args.join(" ")}`;
|
||||
const bashCommand = `
|
||||
echo "Starting heroku build..." >> ${logPath};
|
||||
${command} >> ${logPath} 2>> ${logPath} || {
|
||||
echo "❌ Heroku build failed" >> ${logPath};
|
||||
echo "Starting heroku build..." ;
|
||||
${command} || {
|
||||
echo "❌ Heroku build failed" ;
|
||||
exit 1;
|
||||
}
|
||||
echo "✅ Heroku build completed." >> ${logPath};
|
||||
echo "✅ Heroku build completed." ;
|
||||
`;
|
||||
|
||||
return bashCommand;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import type { InferResultType } from "@dokploy/server/types/with";
|
||||
import type { CreateServiceOptions } from "dockerode";
|
||||
import { uploadImage, uploadImageRemoteCommand } from "../cluster/upload";
|
||||
import { uploadImageRemoteCommand } from "../cluster/upload";
|
||||
import {
|
||||
calculateResources,
|
||||
generateBindMounts,
|
||||
@@ -11,12 +10,12 @@ import {
|
||||
prepareEnvironmentVariables,
|
||||
} from "../docker/utils";
|
||||
import { getRemoteDocker } from "../servers/remote-docker";
|
||||
import { buildCustomDocker, getDockerCommand } from "./docker-file";
|
||||
import { buildHeroku, getHerokuCommand } from "./heroku";
|
||||
import { buildNixpacks, getNixpacksCommand } from "./nixpacks";
|
||||
import { buildPaketo, getPaketoCommand } from "./paketo";
|
||||
import { buildRailpack, getRailpackCommand } from "./railpack";
|
||||
import { buildStatic, getStaticCommand } from "./static";
|
||||
import { getDockerCommand } from "./docker-file";
|
||||
import { getHerokuCommand } from "./heroku";
|
||||
import { getNixpacksCommand } from "./nixpacks";
|
||||
import { getPaketoCommand } from "./paketo";
|
||||
import { getRailpackCommand } from "./railpack";
|
||||
import { getStaticCommand } from "./static";
|
||||
|
||||
// NIXPACKS codeDirectory = where is the path of the code directory
|
||||
// HEROKU codeDirectory = where is the path of the code directory
|
||||
@@ -34,76 +33,35 @@ export type ApplicationNested = InferResultType<
|
||||
}
|
||||
>;
|
||||
|
||||
export const buildApplication = async (
|
||||
application: ApplicationNested,
|
||||
logPath: string,
|
||||
) => {
|
||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||
const { buildType, sourceType } = application;
|
||||
try {
|
||||
writeStream.write(
|
||||
`\nBuild ${buildType}: ✅\nSource Type: ${sourceType}: ✅\n`,
|
||||
);
|
||||
console.log(`Build ${buildType}: ✅`);
|
||||
if (buildType === "nixpacks") {
|
||||
await buildNixpacks(application, writeStream);
|
||||
} else if (buildType === "heroku_buildpacks") {
|
||||
await buildHeroku(application, writeStream);
|
||||
} else if (buildType === "paketo_buildpacks") {
|
||||
await buildPaketo(application, writeStream);
|
||||
} else if (buildType === "dockerfile") {
|
||||
await buildCustomDocker(application, writeStream);
|
||||
} else if (buildType === "static") {
|
||||
await buildStatic(application, writeStream);
|
||||
} else if (buildType === "railpack") {
|
||||
await buildRailpack(application, writeStream);
|
||||
}
|
||||
|
||||
if (application.registryId) {
|
||||
await uploadImage(application, writeStream);
|
||||
}
|
||||
await mechanizeDockerContainer(application);
|
||||
writeStream.write("Docker Deployed: ✅");
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
writeStream.write(`Error ❌\n${error?.message}`);
|
||||
} else {
|
||||
writeStream.write("Error ❌");
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
}
|
||||
};
|
||||
|
||||
export const getBuildCommand = (
|
||||
application: ApplicationNested,
|
||||
logPath: string,
|
||||
) => {
|
||||
export const getBuildCommand = (application: ApplicationNested) => {
|
||||
let command = "";
|
||||
const { buildType, registry } = application;
|
||||
|
||||
if (application.sourceType === "docker") {
|
||||
return "";
|
||||
}
|
||||
switch (buildType) {
|
||||
case "nixpacks":
|
||||
command = getNixpacksCommand(application, logPath);
|
||||
command = getNixpacksCommand(application);
|
||||
break;
|
||||
case "heroku_buildpacks":
|
||||
command = getHerokuCommand(application, logPath);
|
||||
command = getHerokuCommand(application);
|
||||
break;
|
||||
case "paketo_buildpacks":
|
||||
command = getPaketoCommand(application, logPath);
|
||||
command = getPaketoCommand(application);
|
||||
break;
|
||||
case "static":
|
||||
command = getStaticCommand(application, logPath);
|
||||
command = getStaticCommand(application);
|
||||
break;
|
||||
case "dockerfile":
|
||||
command = getDockerCommand(application, logPath);
|
||||
command = getDockerCommand(application);
|
||||
break;
|
||||
case "railpack":
|
||||
command = getRailpackCommand(application, logPath);
|
||||
command = getRailpackCommand(application);
|
||||
break;
|
||||
}
|
||||
if (registry) {
|
||||
command += uploadImageRemoteCommand(application, logPath);
|
||||
command += uploadImageRemoteCommand(application);
|
||||
}
|
||||
|
||||
return command;
|
||||
|
||||
@@ -1,101 +1,11 @@
|
||||
import { existsSync, mkdirSync, type WriteStream } from "node:fs";
|
||||
import path from "node:path";
|
||||
import {
|
||||
buildStatic,
|
||||
getStaticCommand,
|
||||
} from "@dokploy/server/utils/builders/static";
|
||||
import { getStaticCommand } from "@dokploy/server/utils/builders/static";
|
||||
import { nanoid } from "nanoid";
|
||||
import { prepareEnvironmentVariables } from "../docker/utils";
|
||||
import { getBuildAppDirectory } from "../filesystem/directory";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
import type { ApplicationNested } from ".";
|
||||
|
||||
export const buildNixpacks = async (
|
||||
application: ApplicationNested,
|
||||
writeStream: WriteStream,
|
||||
) => {
|
||||
const { env, appName, publishDirectory, cleanCache } = application;
|
||||
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
const buildContainerId = `${appName}-${nanoid(10)}`;
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
env,
|
||||
application.environment.project.env,
|
||||
application.environment.env,
|
||||
);
|
||||
|
||||
const writeToStream = (data: string) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const args = ["build", buildAppDirectory, "--name", appName];
|
||||
|
||||
if (cleanCache) {
|
||||
args.push("--no-cache");
|
||||
}
|
||||
|
||||
for (const env of envVariables) {
|
||||
args.push("--env", env);
|
||||
}
|
||||
|
||||
if (publishDirectory) {
|
||||
/* No need for any start command, since we'll use nginx later on */
|
||||
args.push("--no-error-without-start");
|
||||
}
|
||||
|
||||
await spawnAsync("nixpacks", args, writeToStream);
|
||||
|
||||
/*
|
||||
Run the container with the image created by nixpacks,
|
||||
and copy the artifacts on the host filesystem.
|
||||
Then, remove the container and create a static build.
|
||||
*/
|
||||
if (publishDirectory) {
|
||||
await spawnAsync(
|
||||
"docker",
|
||||
["create", "--name", buildContainerId, appName],
|
||||
writeToStream,
|
||||
);
|
||||
|
||||
const localPath = path.join(buildAppDirectory, publishDirectory);
|
||||
|
||||
if (!existsSync(path.dirname(localPath))) {
|
||||
mkdirSync(path.dirname(localPath), { recursive: true });
|
||||
}
|
||||
|
||||
// https://docs.docker.com/reference/cli/docker/container/cp/
|
||||
const isDirectory =
|
||||
publishDirectory.endsWith("/") || !path.extname(publishDirectory);
|
||||
|
||||
await spawnAsync(
|
||||
"docker",
|
||||
[
|
||||
"cp",
|
||||
`${buildContainerId}:/app/${publishDirectory}${isDirectory ? "/." : ""}`,
|
||||
localPath,
|
||||
],
|
||||
writeToStream,
|
||||
);
|
||||
|
||||
await spawnAsync("docker", ["rm", buildContainerId], writeToStream);
|
||||
|
||||
await buildStatic(application, writeStream);
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
await spawnAsync("docker", ["rm", buildContainerId], writeToStream);
|
||||
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const getNixpacksCommand = (
|
||||
application: ApplicationNested,
|
||||
logPath: string,
|
||||
) => {
|
||||
export const getNixpacksCommand = (application: ApplicationNested) => {
|
||||
const { env, appName, publishDirectory, cleanCache } = application;
|
||||
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
@@ -122,12 +32,12 @@ export const getNixpacksCommand = (
|
||||
}
|
||||
const command = `nixpacks ${args.join(" ")}`;
|
||||
let bashCommand = `
|
||||
echo "Starting nixpacks build..." >> ${logPath};
|
||||
${command} >> ${logPath} 2>> ${logPath} || {
|
||||
echo "❌ Nixpacks build failed" >> ${logPath};
|
||||
exit 1;
|
||||
}
|
||||
echo "✅ Nixpacks build completed." >> ${logPath};
|
||||
echo "Starting nixpacks build..." ;
|
||||
${command} || {
|
||||
echo "❌ Nixpacks build failed" ;
|
||||
exit 1;
|
||||
}
|
||||
echo "✅ Nixpacks build completed." ;
|
||||
`;
|
||||
|
||||
/*
|
||||
@@ -141,16 +51,16 @@ echo "✅ Nixpacks build completed." >> ${logPath};
|
||||
publishDirectory.endsWith("/") || !path.extname(publishDirectory);
|
||||
|
||||
bashCommand += `
|
||||
docker create --name ${buildContainerId} ${appName}
|
||||
mkdir -p ${localPath}
|
||||
docker cp ${buildContainerId}:/app/${publishDirectory}${isDirectory ? "/." : ""} ${path.join(buildAppDirectory, publishDirectory)} >> ${logPath} 2>> ${logPath} || {
|
||||
docker create --name ${buildContainerId} ${appName}
|
||||
mkdir -p ${localPath}
|
||||
docker cp ${buildContainerId}:/app/${publishDirectory}${isDirectory ? "/." : ""} ${path.join(buildAppDirectory, publishDirectory)} || {
|
||||
docker rm ${buildContainerId}
|
||||
echo "❌ Copying ${publishDirectory} to ${path.join(buildAppDirectory, publishDirectory)} failed" ;
|
||||
exit 1;
|
||||
}
|
||||
docker rm ${buildContainerId}
|
||||
echo "❌ Copying ${publishDirectory} to ${path.join(buildAppDirectory, publishDirectory)} failed" >> ${logPath};
|
||||
exit 1;
|
||||
}
|
||||
docker rm ${buildContainerId}
|
||||
${getStaticCommand(application, logPath)}
|
||||
`;
|
||||
${getStaticCommand(application)}
|
||||
`;
|
||||
}
|
||||
|
||||
return bashCommand;
|
||||
|
||||
@@ -1,53 +1,8 @@
|
||||
import type { WriteStream } from "node:fs";
|
||||
import { prepareEnvironmentVariables } from "../docker/utils";
|
||||
import { getBuildAppDirectory } from "../filesystem/directory";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
import type { ApplicationNested } from ".";
|
||||
|
||||
export const buildPaketo = async (
|
||||
application: ApplicationNested,
|
||||
writeStream: WriteStream,
|
||||
) => {
|
||||
const { env, appName, cleanCache } = application;
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
env,
|
||||
application.environment.project.env,
|
||||
application.environment.env,
|
||||
);
|
||||
try {
|
||||
const args = [
|
||||
"build",
|
||||
appName,
|
||||
"--path",
|
||||
buildAppDirectory,
|
||||
"--builder",
|
||||
"paketobuildpacks/builder-jammy-full",
|
||||
];
|
||||
|
||||
if (cleanCache) {
|
||||
args.push("--clear-cache");
|
||||
}
|
||||
|
||||
for (const env of envVariables) {
|
||||
args.push("--env", env);
|
||||
}
|
||||
|
||||
await spawnAsync("pack", args, (data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const getPaketoCommand = (
|
||||
application: ApplicationNested,
|
||||
logPath: string,
|
||||
) => {
|
||||
export const getPaketoCommand = (application: ApplicationNested) => {
|
||||
const { env, appName, cleanCache } = application;
|
||||
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
@@ -76,12 +31,12 @@ export const getPaketoCommand = (
|
||||
|
||||
const command = `pack ${args.join(" ")}`;
|
||||
const bashCommand = `
|
||||
echo "Starting Paketo build..." >> ${logPath};
|
||||
${command} >> ${logPath} 2>> ${logPath} || {
|
||||
echo "❌ Paketo build failed" >> ${logPath};
|
||||
echo "Starting Paketo build..." ;
|
||||
${command} || {
|
||||
echo "❌ Paketo build failed" ;
|
||||
exit 1;
|
||||
}
|
||||
echo "✅ Paketo build completed." >> ${logPath};
|
||||
echo "✅ Paketo build completed." ;
|
||||
`;
|
||||
|
||||
return bashCommand;
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import type { WriteStream } from "node:fs";
|
||||
import { nanoid } from "nanoid";
|
||||
import {
|
||||
parseEnvironmentKeyValuePair,
|
||||
prepareEnvironmentVariables,
|
||||
} from "../docker/utils";
|
||||
import { getBuildAppDirectory } from "../filesystem/directory";
|
||||
import { execAsync } from "../process/execAsync";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
import type { ApplicationNested } from ".";
|
||||
|
||||
const calculateSecretsHash = (envVariables: string[]): string => {
|
||||
@@ -18,108 +15,7 @@ const calculateSecretsHash = (envVariables: string[]): string => {
|
||||
return hash.digest("hex");
|
||||
};
|
||||
|
||||
export const buildRailpack = async (
|
||||
application: ApplicationNested,
|
||||
writeStream: WriteStream,
|
||||
) => {
|
||||
const { env, appName, cleanCache } = application;
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
env,
|
||||
application.environment.project.env,
|
||||
application.environment.env,
|
||||
);
|
||||
|
||||
try {
|
||||
await execAsync(
|
||||
"docker buildx create --use --name builder-containerd --driver docker-container || true",
|
||||
);
|
||||
|
||||
await execAsync("docker buildx use builder-containerd");
|
||||
|
||||
// First prepare the build plan and info
|
||||
const prepareArgs = [
|
||||
"prepare",
|
||||
buildAppDirectory,
|
||||
"--plan-out",
|
||||
`${buildAppDirectory}/railpack-plan.json`,
|
||||
"--info-out",
|
||||
`${buildAppDirectory}/railpack-info.json`,
|
||||
];
|
||||
|
||||
// Add environment variables to prepare command
|
||||
for (const env of envVariables) {
|
||||
prepareArgs.push("--env", env);
|
||||
}
|
||||
|
||||
// Run prepare command
|
||||
await spawnAsync("railpack", prepareArgs, (data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
});
|
||||
|
||||
// Calculate secrets hash for layer invalidation
|
||||
const secretsHash = calculateSecretsHash(envVariables);
|
||||
|
||||
// Build with BuildKit using the Railpack frontend
|
||||
const cacheKey = cleanCache ? nanoid(10) : undefined;
|
||||
const buildArgs = [
|
||||
"buildx",
|
||||
"build",
|
||||
...(cacheKey
|
||||
? [
|
||||
"--build-arg",
|
||||
`secrets-hash=${secretsHash}`,
|
||||
"--build-arg",
|
||||
`cache-key=${cacheKey}`,
|
||||
]
|
||||
: []),
|
||||
"--build-arg",
|
||||
`BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v${application.railpackVersion}`,
|
||||
"-f",
|
||||
`${buildAppDirectory}/railpack-plan.json`,
|
||||
"--output",
|
||||
`type=docker,name=${appName}`,
|
||||
];
|
||||
|
||||
// Add secrets properly formatted
|
||||
const env: { [key: string]: string } = {};
|
||||
for (const pair of envVariables) {
|
||||
const [key, value] = parseEnvironmentKeyValuePair(pair);
|
||||
if (key && value) {
|
||||
buildArgs.push("--secret", `id=${key},env=${key}`);
|
||||
env[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
buildArgs.push(buildAppDirectory);
|
||||
|
||||
await spawnAsync(
|
||||
"docker",
|
||||
buildArgs,
|
||||
(data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
},
|
||||
{
|
||||
env: { ...process.env, ...env },
|
||||
},
|
||||
);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
} finally {
|
||||
await execAsync("docker buildx rm builder-containerd");
|
||||
}
|
||||
};
|
||||
|
||||
export const getRailpackCommand = (
|
||||
application: ApplicationNested,
|
||||
logPath: string,
|
||||
) => {
|
||||
export const getRailpackCommand = (application: ApplicationNested) => {
|
||||
const { env, appName, cleanCache } = application;
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
@@ -179,25 +75,28 @@ export const getRailpackCommand = (
|
||||
buildArgs.push(buildAppDirectory);
|
||||
|
||||
const bashCommand = `
|
||||
|
||||
# Ensure we have a builder with containerd
|
||||
docker buildx create --use --name builder-containerd --driver docker-container || true
|
||||
docker buildx use builder-containerd
|
||||
|
||||
echo "Preparing Railpack build plan..." >> "${logPath}";
|
||||
railpack ${prepareArgs.join(" ")} >> ${logPath} 2>> ${logPath} || {
|
||||
echo "❌ Railpack prepare failed" >> ${logPath};
|
||||
echo "Preparing Railpack build plan..." ;
|
||||
railpack ${prepareArgs.join(" ")} || {
|
||||
echo "❌ Railpack prepare failed" ;
|
||||
docker buildx rm builder-containerd || true
|
||||
exit 1;
|
||||
}
|
||||
echo "✅ Railpack prepare completed." >> ${logPath};
|
||||
echo "✅ Railpack prepare completed." ;
|
||||
|
||||
echo "Building with Railpack frontend..." >> "${logPath}";
|
||||
echo "Building with Railpack frontend..." ;
|
||||
# Export environment variables for secrets
|
||||
${exportEnvs.join("\n")}
|
||||
docker ${buildArgs.join(" ")} >> ${logPath} 2>> ${logPath} || {
|
||||
echo "❌ Railpack build failed" >> ${logPath};
|
||||
docker ${buildArgs.join(" ")} || {
|
||||
echo "❌ Railpack build failed" ;
|
||||
docker buildx rm builder-containerd || true
|
||||
exit 1;
|
||||
}
|
||||
echo "✅ Railpack build completed." >> ${logPath};
|
||||
echo "✅ Railpack build completed." ;
|
||||
docker buildx rm builder-containerd
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import type { WriteStream } from "node:fs";
|
||||
import {
|
||||
buildCustomDocker,
|
||||
getDockerCommand,
|
||||
} from "@dokploy/server/utils/builders/docker-file";
|
||||
import { createFile, getCreateFileCommand } from "../docker/utils";
|
||||
import { getDockerCommand } from "@dokploy/server/utils/builders/docker-file";
|
||||
import { getCreateFileCommand } from "../docker/utils";
|
||||
import { getBuildAppDirectory } from "../filesystem/directory";
|
||||
import type { ApplicationNested } from ".";
|
||||
|
||||
@@ -32,81 +28,40 @@ http {
|
||||
}
|
||||
`;
|
||||
|
||||
export const buildStatic = async (
|
||||
application: ApplicationNested,
|
||||
writeStream: WriteStream,
|
||||
) => {
|
||||
export const getStaticCommand = (application: ApplicationNested) => {
|
||||
const { publishDirectory, isStaticSpa } = application;
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
|
||||
try {
|
||||
if (isStaticSpa) {
|
||||
createFile(buildAppDirectory, "nginx.conf", nginxSpaConfig);
|
||||
}
|
||||
|
||||
createFile(
|
||||
let command = "";
|
||||
if (isStaticSpa) {
|
||||
command += getCreateFileCommand(
|
||||
buildAppDirectory,
|
||||
".dockerignore",
|
||||
[".git", ".env", "Dockerfile", ".dockerignore"].join("\n"),
|
||||
"nginx.conf",
|
||||
nginxSpaConfig,
|
||||
);
|
||||
|
||||
createFile(
|
||||
buildAppDirectory,
|
||||
"Dockerfile",
|
||||
[
|
||||
"FROM nginx:alpine",
|
||||
"WORKDIR /usr/share/nginx/html/",
|
||||
isStaticSpa ? "COPY nginx.conf /etc/nginx/nginx.conf" : "",
|
||||
`COPY ${publishDirectory || "."} .`,
|
||||
'CMD ["nginx", "-g", "daemon off;"]',
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
createFile(
|
||||
buildAppDirectory,
|
||||
".dockerignore",
|
||||
[".git", ".env", "Dockerfile", ".dockerignore"].join("\n"),
|
||||
);
|
||||
|
||||
await buildCustomDocker(
|
||||
{
|
||||
...application,
|
||||
buildType: "dockerfile",
|
||||
dockerfile: "Dockerfile",
|
||||
},
|
||||
writeStream,
|
||||
);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const getStaticCommand = (
|
||||
application: ApplicationNested,
|
||||
logPath: string,
|
||||
) => {
|
||||
const { publishDirectory } = application;
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
command += getCreateFileCommand(
|
||||
buildAppDirectory,
|
||||
".dockerignore",
|
||||
[".git", ".env", "Dockerfile", ".dockerignore"].join("\n"),
|
||||
);
|
||||
|
||||
let command = getCreateFileCommand(
|
||||
command += getCreateFileCommand(
|
||||
buildAppDirectory,
|
||||
"Dockerfile",
|
||||
[
|
||||
"FROM nginx:alpine",
|
||||
"WORKDIR /usr/share/nginx/html/",
|
||||
isStaticSpa ? "COPY nginx.conf /etc/nginx/nginx.conf" : "",
|
||||
`COPY ${publishDirectory || "."} .`,
|
||||
'CMD ["nginx", "-g", "daemon off;"]',
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
command += getDockerCommand(
|
||||
{
|
||||
...application,
|
||||
buildType: "dockerfile",
|
||||
dockerfile: "Dockerfile",
|
||||
},
|
||||
logPath,
|
||||
);
|
||||
command += getDockerCommand({
|
||||
...application,
|
||||
buildType: "dockerfile",
|
||||
dockerfile: "Dockerfile",
|
||||
});
|
||||
return command;
|
||||
};
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import type { WriteStream } from "node:fs";
|
||||
import type { ApplicationNested } from "../builders";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
|
||||
export const uploadImage = async (
|
||||
application: ApplicationNested,
|
||||
writeStream: WriteStream,
|
||||
) => {
|
||||
export const uploadImageRemoteCommand = (application: ApplicationNested) => {
|
||||
const registry = application.registry;
|
||||
|
||||
if (!registry) {
|
||||
@@ -19,85 +14,28 @@ export const uploadImage = async (
|
||||
const finalURL = registryUrl;
|
||||
|
||||
// Build registry tag in correct format: registry.com/owner/image:tag
|
||||
// For ghcr.io: ghcr.io/username/image:tag
|
||||
// For docker.io: docker.io/username/image:tag
|
||||
const registryTag = imagePrefix
|
||||
? `${registryUrl ? `${registryUrl}/` : ""}${imagePrefix}/${imageName}`
|
||||
: `${registryUrl ? `${registryUrl}/` : ""}${username}/${imageName}`;
|
||||
|
||||
try {
|
||||
writeStream.write(
|
||||
`📦 [Enabled Registry] Uploading image to ${registry.registryType} | ${imageName} | ${finalURL} | ${registryTag}\n`,
|
||||
);
|
||||
const loginCommand = spawnAsync(
|
||||
"docker",
|
||||
["login", finalURL, "-u", registry.username, "--password-stdin"],
|
||||
(data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
},
|
||||
);
|
||||
loginCommand.child?.stdin?.write(registry.password);
|
||||
loginCommand.child?.stdin?.end();
|
||||
await loginCommand;
|
||||
|
||||
await spawnAsync("docker", ["tag", imageName, registryTag], (data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
});
|
||||
|
||||
await spawnAsync("docker", ["push", registryTag], (data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const uploadImageRemoteCommand = (
|
||||
application: ApplicationNested,
|
||||
logPath: string,
|
||||
) => {
|
||||
const registry = application.registry;
|
||||
|
||||
if (!registry) {
|
||||
throw new Error("Registry not found");
|
||||
}
|
||||
|
||||
const { registryUrl, imagePrefix, username } = registry;
|
||||
const { appName } = application;
|
||||
const imageName = `${appName}:latest`;
|
||||
|
||||
const finalURL = registryUrl;
|
||||
|
||||
// Build registry tag in correct format: registry.com/owner/image:tag
|
||||
const registryTag = imagePrefix
|
||||
? `${registryUrl}/${imagePrefix}/${imageName}`
|
||||
: `${registryUrl}/${username}/${imageName}`;
|
||||
|
||||
try {
|
||||
const command = `
|
||||
echo "📦 [Enabled Registry] Uploading image to '${registry.registryType}' | '${registryTag}'" >> ${logPath};
|
||||
echo "${registry.password}" | docker login ${finalURL} -u ${registry.username} --password-stdin >> ${logPath} 2>> ${logPath} || {
|
||||
echo "❌ DockerHub Failed" >> ${logPath};
|
||||
echo "📦 [Enabled Registry] Uploading image to '${registry.registryType}' | '${registryTag}'" ;
|
||||
echo "${registry.password}" | docker login ${finalURL} -u ${registry.username} --password-stdin || {
|
||||
echo "❌ DockerHub Failed" ;
|
||||
exit 1;
|
||||
}
|
||||
echo "✅ Registry Login Success" >> ${logPath};
|
||||
docker tag ${imageName} ${registryTag} >> ${logPath} 2>> ${logPath} || {
|
||||
echo "❌ Error tagging image" >> ${logPath};
|
||||
echo "✅ Registry Login Success" ;
|
||||
docker tag ${imageName} ${registryTag} || {
|
||||
echo "❌ Error tagging image" ;
|
||||
exit 1;
|
||||
}
|
||||
echo "✅ Image Tagged" >> ${logPath};
|
||||
docker push ${registryTag} 2>> ${logPath} || {
|
||||
echo "❌ Error pushing image" >> ${logPath};
|
||||
echo "✅ Image Tagged" ;
|
||||
docker push ${registryTag} || {
|
||||
echo "❌ Error pushing image" ;
|
||||
exit 1;
|
||||
}
|
||||
echo "✅ Image Pushed" >> ${logPath};
|
||||
echo "✅ Image Pushed" ;
|
||||
`;
|
||||
return command;
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { findComposeById } from "@dokploy/server/services/compose";
|
||||
import { stringify } from "yaml";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { addAppNameToAllServiceNames } from "./collision/root-network";
|
||||
import { generateRandomHash } from "./compose";
|
||||
import { addSuffixToAllVolumes } from "./compose/volume";
|
||||
import {
|
||||
cloneCompose,
|
||||
cloneComposeRemote,
|
||||
loadDockerCompose,
|
||||
loadDockerComposeRemote,
|
||||
} from "./domain";
|
||||
@@ -31,10 +31,11 @@ export const randomizeIsolatedDeploymentComposeFile = async (
|
||||
) => {
|
||||
const compose = await findComposeById(composeId);
|
||||
|
||||
const command = await cloneCompose(compose);
|
||||
if (compose.serverId) {
|
||||
await cloneComposeRemote(compose);
|
||||
await execAsyncRemote(compose.serverId, command);
|
||||
} else {
|
||||
await cloneCompose(compose);
|
||||
await execAsync(command);
|
||||
}
|
||||
|
||||
let composeData: ComposeSpecification | null;
|
||||
|
||||
@@ -1,35 +1,16 @@
|
||||
import fs, { existsSync, readFileSync } from "node:fs";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import type { Compose } from "@dokploy/server/services/compose";
|
||||
import type { Domain } from "@dokploy/server/services/domain";
|
||||
import { parse, stringify } from "yaml";
|
||||
import { execAsyncRemote } from "../process/execAsync";
|
||||
import {
|
||||
cloneRawBitbucketRepository,
|
||||
cloneRawBitbucketRepositoryRemote,
|
||||
} from "../providers/bitbucket";
|
||||
import {
|
||||
cloneGitRawRepository,
|
||||
cloneRawGitRepositoryRemote,
|
||||
} from "../providers/git";
|
||||
import {
|
||||
cloneRawGiteaRepository,
|
||||
cloneRawGiteaRepositoryRemote,
|
||||
} from "../providers/gitea";
|
||||
import {
|
||||
cloneRawGithubRepository,
|
||||
cloneRawGithubRepositoryRemote,
|
||||
} from "../providers/github";
|
||||
import {
|
||||
cloneRawGitlabRepository,
|
||||
cloneRawGitlabRepositoryRemote,
|
||||
} from "../providers/gitlab";
|
||||
import {
|
||||
createComposeFileRaw,
|
||||
createComposeFileRawRemote,
|
||||
} from "../providers/raw";
|
||||
import { cloneBitbucketRepository } from "../providers/bitbucket";
|
||||
import { cloneGitRepository } from "../providers/git";
|
||||
import { cloneGiteaRepository } from "../providers/gitea";
|
||||
import { cloneGithubRepository } from "../providers/github";
|
||||
import { cloneGitlabRepository } from "../providers/gitlab";
|
||||
import { getCreateComposeFileCommand } from "../providers/raw";
|
||||
import { randomizeDeployableSpecificationFile } from "./collision";
|
||||
import { randomizeSpecificationFile } from "./compose";
|
||||
import type {
|
||||
@@ -40,35 +21,25 @@ import type {
|
||||
import { encodeBase64 } from "./utils";
|
||||
|
||||
export const cloneCompose = async (compose: Compose) => {
|
||||
let command = "set -e;";
|
||||
const entity = {
|
||||
...compose,
|
||||
type: "compose" as const,
|
||||
};
|
||||
if (compose.sourceType === "github") {
|
||||
await cloneRawGithubRepository(compose);
|
||||
command += await cloneGithubRepository(entity);
|
||||
} else if (compose.sourceType === "gitlab") {
|
||||
await cloneRawGitlabRepository(compose);
|
||||
command += await cloneGitlabRepository(entity);
|
||||
} else if (compose.sourceType === "bitbucket") {
|
||||
await cloneRawBitbucketRepository(compose);
|
||||
command += await cloneBitbucketRepository(entity);
|
||||
} else if (compose.sourceType === "git") {
|
||||
await cloneGitRawRepository(compose);
|
||||
command += await cloneGitRepository(entity);
|
||||
} else if (compose.sourceType === "gitea") {
|
||||
await cloneRawGiteaRepository(compose);
|
||||
command += await cloneGiteaRepository(entity);
|
||||
} else if (compose.sourceType === "raw") {
|
||||
await createComposeFileRaw(compose);
|
||||
}
|
||||
};
|
||||
|
||||
export const cloneComposeRemote = async (compose: Compose) => {
|
||||
if (compose.sourceType === "github") {
|
||||
await cloneRawGithubRepositoryRemote(compose);
|
||||
} else if (compose.sourceType === "gitlab") {
|
||||
await cloneRawGitlabRepositoryRemote(compose);
|
||||
} else if (compose.sourceType === "bitbucket") {
|
||||
await cloneRawBitbucketRepositoryRemote(compose);
|
||||
} else if (compose.sourceType === "git") {
|
||||
await cloneRawGitRepositoryRemote(compose);
|
||||
} else if (compose.sourceType === "gitea") {
|
||||
await cloneRawGiteaRepositoryRemote(compose);
|
||||
} else if (compose.sourceType === "raw") {
|
||||
await createComposeFileRawRemote(compose);
|
||||
command += getCreateComposeFileCommand(compose);
|
||||
}
|
||||
return command;
|
||||
};
|
||||
|
||||
export const getComposePath = (compose: Compose) => {
|
||||
@@ -131,28 +102,9 @@ export const readComposeFile = async (compose: Compose) => {
|
||||
return null;
|
||||
};
|
||||
|
||||
export const writeDomainsToCompose = async (
|
||||
compose: Compose,
|
||||
domains: Domain[],
|
||||
) => {
|
||||
if (!domains.length) {
|
||||
return;
|
||||
}
|
||||
const composeConverted = await addDomainToCompose(compose, domains);
|
||||
|
||||
const path = getComposePath(compose);
|
||||
const composeString = stringify(composeConverted, { lineWidth: 1000 });
|
||||
try {
|
||||
await writeFile(path, composeString, "utf8");
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const writeDomainsToComposeRemote = async (
|
||||
compose: Compose,
|
||||
domains: Domain[],
|
||||
logPath: string,
|
||||
) => {
|
||||
if (!domains.length) {
|
||||
return "";
|
||||
@@ -164,7 +116,7 @@ export const writeDomainsToComposeRemote = async (
|
||||
|
||||
if (!composeConverted) {
|
||||
return `
|
||||
echo "❌ Error: Compose file not found" >> ${logPath};
|
||||
echo "❌ Error: Compose file not found";
|
||||
exit 1;
|
||||
`;
|
||||
}
|
||||
@@ -175,12 +127,13 @@ exit 1;
|
||||
}
|
||||
} catch (error) {
|
||||
// @ts-ignore
|
||||
return `echo "❌ Has occured an error: ${error?.message || error}" >> ${logPath};
|
||||
return `echo "❌ Has occured an error: ${error?.message || error}";
|
||||
exit 1;
|
||||
`;
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
// (node:59875) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 SIGTERM listeners added to [process]. Use emitter.setMaxListeners() to increase limit
|
||||
export const addDomainToCompose = async (
|
||||
compose: Compose,
|
||||
domains: Domain[],
|
||||
@@ -190,7 +143,7 @@ export const addDomainToCompose = async (
|
||||
let result: ComposeSpecification | null;
|
||||
|
||||
if (compose.serverId) {
|
||||
result = await loadDockerComposeRemote(compose); // aca hay que ir al servidor e ir a traer el compose file al servidor
|
||||
result = await loadDockerComposeRemote(compose);
|
||||
} else {
|
||||
result = await loadDockerCompose(compose);
|
||||
}
|
||||
|
||||
@@ -116,11 +116,7 @@ export const execAsyncRemote = async (
|
||||
if (code === 0) {
|
||||
resolve({ stdout, stderr });
|
||||
} else {
|
||||
reject(
|
||||
new Error(
|
||||
`Command exited with code ${code}. Stderr: ${stderr}, command: ${command}`,
|
||||
),
|
||||
);
|
||||
reject(new Error(`Error occurred ❌: ${stderr}`));
|
||||
}
|
||||
})
|
||||
.on("data", (data: string) => {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import type {
|
||||
@@ -9,12 +8,8 @@ import {
|
||||
type Bitbucket,
|
||||
findBitbucketById,
|
||||
} from "@dokploy/server/services/bitbucket";
|
||||
import type { Compose } from "@dokploy/server/services/compose";
|
||||
import type { InferResultType } from "@dokploy/server/types/with";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { recreateDirectory } from "../filesystem/directory";
|
||||
import { execAsyncRemote } from "../process/execAsync";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
|
||||
export type ApplicationWithBitbucket = InferResultType<
|
||||
"applications",
|
||||
@@ -81,202 +76,52 @@ export const getBitbucketHeaders = (bitbucketProvider: Bitbucket) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const cloneBitbucketRepository = async (
|
||||
entity: ApplicationWithBitbucket | ComposeWithBitbucket,
|
||||
logPath: string,
|
||||
isCompose = false,
|
||||
) => {
|
||||
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths();
|
||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||
interface CloneBitbucketRepository {
|
||||
appName: string;
|
||||
bitbucketRepository: string | null;
|
||||
bitbucketOwner: string | null;
|
||||
bitbucketBranch: string | null;
|
||||
bitbucketId: string | null;
|
||||
enableSubmodules: boolean;
|
||||
serverId: string | null;
|
||||
type?: "application" | "compose";
|
||||
}
|
||||
|
||||
export const cloneBitbucketRepository = async ({
|
||||
type = "application",
|
||||
...entity
|
||||
}: CloneBitbucketRepository) => {
|
||||
let command = "set -e;";
|
||||
const {
|
||||
appName,
|
||||
bitbucketRepository,
|
||||
bitbucketOwner,
|
||||
bitbucketBranch,
|
||||
bitbucketId,
|
||||
bitbucket,
|
||||
enableSubmodules,
|
||||
serverId,
|
||||
} = entity;
|
||||
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(!!serverId);
|
||||
|
||||
if (!bitbucketId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Bitbucket Provider not found",
|
||||
});
|
||||
command += `echo "Error: ❌ Bitbucket Provider not found"; exit 1;`;
|
||||
return command;
|
||||
}
|
||||
const bitbucket = await findBitbucketById(bitbucketId);
|
||||
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
if (!bitbucket) {
|
||||
command += `echo "Error: ❌ Bitbucket Provider not found"; exit 1;`;
|
||||
return command;
|
||||
}
|
||||
const basePath = type === "compose" ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
command += `rm -rf ${outputPath};`;
|
||||
command += `mkdir -p ${outputPath};`;
|
||||
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
|
||||
const cloneUrl = getBitbucketCloneUrl(bitbucket, repoclone);
|
||||
try {
|
||||
writeStream.write(`\nCloning Repo ${repoclone} to ${outputPath}: ✅\n`);
|
||||
const cloneArgs = [
|
||||
"clone",
|
||||
"--branch",
|
||||
bitbucketBranch!,
|
||||
"--depth",
|
||||
"1",
|
||||
...(enableSubmodules ? ["--recurse-submodules"] : []),
|
||||
cloneUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
];
|
||||
|
||||
await spawnAsync("git", cloneArgs, (data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
});
|
||||
writeStream.write(`\nCloned ${repoclone} to ${outputPath}: ✅\n`);
|
||||
} catch (error) {
|
||||
writeStream.write(`ERROR Cloning: ${error}: ❌`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
}
|
||||
};
|
||||
|
||||
export const cloneRawBitbucketRepository = async (entity: Compose) => {
|
||||
const { COMPOSE_PATH } = paths();
|
||||
const {
|
||||
appName,
|
||||
bitbucketRepository,
|
||||
bitbucketOwner,
|
||||
bitbucketBranch,
|
||||
bitbucketId,
|
||||
enableSubmodules,
|
||||
} = entity;
|
||||
|
||||
if (!bitbucketId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Bitbucket Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
const bitbucketProvider = await findBitbucketById(bitbucketId);
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
|
||||
const cloneUrl = getBitbucketCloneUrl(bitbucketProvider, repoclone);
|
||||
|
||||
try {
|
||||
const cloneArgs = [
|
||||
"clone",
|
||||
"--branch",
|
||||
bitbucketBranch!,
|
||||
"--depth",
|
||||
"1",
|
||||
...(enableSubmodules ? ["--recurse-submodules"] : []),
|
||||
cloneUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
];
|
||||
|
||||
await spawnAsync("git", cloneArgs);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const cloneRawBitbucketRepositoryRemote = async (compose: Compose) => {
|
||||
const { COMPOSE_PATH } = paths(true);
|
||||
const {
|
||||
appName,
|
||||
bitbucketRepository,
|
||||
bitbucketOwner,
|
||||
bitbucketBranch,
|
||||
bitbucketId,
|
||||
serverId,
|
||||
enableSubmodules,
|
||||
} = compose;
|
||||
|
||||
if (!serverId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Server not found",
|
||||
});
|
||||
}
|
||||
if (!bitbucketId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Bitbucket Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
const bitbucketProvider = await findBitbucketById(bitbucketId);
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
|
||||
const cloneUrl = getBitbucketCloneUrl(bitbucketProvider, repoclone);
|
||||
|
||||
try {
|
||||
const cloneCommand = `
|
||||
rm -rf ${outputPath};
|
||||
git clone --branch ${bitbucketBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath}
|
||||
`;
|
||||
await execAsyncRemote(serverId, cloneCommand);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getBitbucketCloneCommand = async (
|
||||
entity: ApplicationWithBitbucket | ComposeWithBitbucket,
|
||||
logPath: string,
|
||||
isCompose = false,
|
||||
) => {
|
||||
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(true);
|
||||
const {
|
||||
appName,
|
||||
bitbucketRepository,
|
||||
bitbucketOwner,
|
||||
bitbucketBranch,
|
||||
bitbucketId,
|
||||
serverId,
|
||||
enableSubmodules,
|
||||
} = entity;
|
||||
|
||||
if (!serverId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Server not found",
|
||||
});
|
||||
}
|
||||
|
||||
if (!bitbucketId) {
|
||||
const command = `
|
||||
echo "Error: ❌ Bitbucket Provider not found" >> ${logPath};
|
||||
exit 1;
|
||||
`;
|
||||
await execAsyncRemote(serverId, command);
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Bitbucket Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
const bitbucketProvider = await findBitbucketById(bitbucketId);
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
|
||||
const cloneUrl = getBitbucketCloneUrl(bitbucketProvider, repoclone);
|
||||
|
||||
const cloneCommand = `
|
||||
rm -rf ${outputPath};
|
||||
mkdir -p ${outputPath};
|
||||
if ! git clone --branch ${bitbucketBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
|
||||
echo "❌ [ERROR] Fail to clone the repository ${repoclone}" >> ${logPath};
|
||||
exit 1;
|
||||
fi
|
||||
echo "Cloned ${repoclone} to ${outputPath}: ✅" >> ${logPath};
|
||||
`;
|
||||
|
||||
return cloneCommand;
|
||||
command += `echo "Cloning Repo ${repoclone} to ${outputPath}: ✅";`;
|
||||
command += `git clone --branch ${bitbucketBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} --progress;`;
|
||||
return command;
|
||||
};
|
||||
|
||||
export const getBitbucketRepositories = async (bitbucketId?: string) => {
|
||||
|
||||
@@ -1,60 +1,6 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { type ApplicationNested, mechanizeDockerContainer } from "../builders";
|
||||
import { pullImage } from "../docker/utils";
|
||||
import type { ApplicationNested } from "../builders";
|
||||
|
||||
interface RegistryAuth {
|
||||
username: string;
|
||||
password: string;
|
||||
registryUrl: string;
|
||||
}
|
||||
|
||||
export const buildDocker = async (
|
||||
application: ApplicationNested,
|
||||
logPath: string,
|
||||
): Promise<void> => {
|
||||
const { buildType, dockerImage, username, password } = application;
|
||||
const authConfig: Partial<RegistryAuth> = {
|
||||
username: username || "",
|
||||
password: password || "",
|
||||
registryUrl: application.registryUrl || "",
|
||||
};
|
||||
|
||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||
|
||||
writeStream.write(`\nBuild ${buildType}\n`);
|
||||
|
||||
writeStream.write(`Pulling ${dockerImage}: ✅\n`);
|
||||
|
||||
try {
|
||||
if (!dockerImage) {
|
||||
throw new Error("Docker image not found");
|
||||
}
|
||||
|
||||
await pullImage(
|
||||
dockerImage,
|
||||
(data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(`${data}\n`);
|
||||
}
|
||||
},
|
||||
authConfig,
|
||||
);
|
||||
await mechanizeDockerContainer(application);
|
||||
writeStream.write("\nDocker Deployed: ✅\n");
|
||||
} catch (error) {
|
||||
writeStream.write(
|
||||
`❌ Error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
}
|
||||
};
|
||||
|
||||
export const buildRemoteDocker = async (
|
||||
application: ApplicationNested,
|
||||
logPath: string,
|
||||
) => {
|
||||
export const buildRemoteDocker = async (application: ApplicationNested) => {
|
||||
const { registryUrl, dockerImage, username, password } = application;
|
||||
|
||||
try {
|
||||
@@ -62,25 +8,25 @@ export const buildRemoteDocker = async (
|
||||
throw new Error("Docker image not found");
|
||||
}
|
||||
let command = `
|
||||
echo "Pulling ${dockerImage}" >> ${logPath};
|
||||
echo "Pulling ${dockerImage}";
|
||||
`;
|
||||
|
||||
if (username && password) {
|
||||
command += `
|
||||
if ! echo "${password}" | docker login --username "${username}" --password-stdin "${registryUrl || ""}" >> ${logPath} 2>&1; then
|
||||
echo "❌ Login failed" >> ${logPath};
|
||||
if ! echo "${password}" | docker login --username "${username}" --password-stdin "${registryUrl || ""}" 2>&1; then
|
||||
echo "❌ Login failed";
|
||||
exit 1;
|
||||
fi
|
||||
`;
|
||||
}
|
||||
|
||||
command += `
|
||||
docker pull ${dockerImage} >> ${logPath} 2>> ${logPath} || {
|
||||
echo "❌ Pulling image failed" >> ${logPath};
|
||||
docker pull ${dockerImage} 2>&1 || {
|
||||
echo "❌ Pulling image failed";
|
||||
exit 1;
|
||||
}
|
||||
|
||||
echo "✅ Pulling image completed." >> ${logPath};
|
||||
echo "✅ Pulling image completed.";
|
||||
`;
|
||||
return command;
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,159 +1,65 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import path, { join } from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import type { Compose } from "@dokploy/server/services/compose";
|
||||
import {
|
||||
findSSHKeyById,
|
||||
updateSSHKeyById,
|
||||
} from "@dokploy/server/services/ssh-key";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { recreateDirectory } from "../filesystem/directory";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
|
||||
export const cloneGitRepository = async (
|
||||
entity: {
|
||||
appName: string;
|
||||
customGitUrl?: string | null;
|
||||
customGitBranch?: string | null;
|
||||
customGitSSHKeyId?: string | null;
|
||||
enableSubmodules?: boolean;
|
||||
},
|
||||
logPath: string,
|
||||
isCompose = false,
|
||||
) => {
|
||||
const { SSH_PATH, COMPOSE_PATH, APPLICATIONS_PATH } = paths();
|
||||
interface CloneGitRepository {
|
||||
appName: string;
|
||||
customGitUrl?: string | null;
|
||||
customGitBranch?: string | null;
|
||||
customGitSSHKeyId?: string | null;
|
||||
enableSubmodules?: boolean;
|
||||
serverId: string | null;
|
||||
type?: "application" | "compose";
|
||||
}
|
||||
|
||||
export const cloneGitRepository = async ({
|
||||
type = "application",
|
||||
...entity
|
||||
}: CloneGitRepository) => {
|
||||
let command = "set -e;";
|
||||
const {
|
||||
appName,
|
||||
customGitUrl,
|
||||
customGitBranch,
|
||||
customGitSSHKeyId,
|
||||
enableSubmodules,
|
||||
serverId,
|
||||
} = entity;
|
||||
const { SSH_PATH, COMPOSE_PATH, APPLICATIONS_PATH } = paths(!!serverId);
|
||||
|
||||
if (!customGitUrl || !customGitBranch) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error: Repository not found",
|
||||
});
|
||||
command += `echo "Error: ❌ Repository not found"; exit 1;`;
|
||||
return command;
|
||||
}
|
||||
|
||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||
const temporalKeyPath = path.join("/tmp", "id_rsa");
|
||||
|
||||
if (customGitSSHKeyId) {
|
||||
const sshKey = await findSSHKeyById(customGitSSHKeyId);
|
||||
|
||||
await execAsync(`
|
||||
command += `
|
||||
echo "${sshKey.privateKey}" > ${temporalKeyPath}
|
||||
chmod 600 ${temporalKeyPath}
|
||||
`);
|
||||
chmod 600 ${temporalKeyPath};
|
||||
`;
|
||||
}
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const basePath = type === "compose" ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
const knownHostsPath = path.join(SSH_PATH, "known_hosts");
|
||||
|
||||
try {
|
||||
if (!isHttpOrHttps(customGitUrl)) {
|
||||
if (!customGitSSHKeyId) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message:
|
||||
"Error: you are trying to clone a ssh repository without a ssh key, please set a ssh key",
|
||||
});
|
||||
}
|
||||
await addHostToKnownHosts(customGitUrl);
|
||||
if (!isHttpOrHttps(customGitUrl)) {
|
||||
if (!customGitSSHKeyId) {
|
||||
command += `echo "Error: ❌ You are trying to clone a ssh repository without a ssh key, please set a ssh key"; exit 1;`;
|
||||
return command;
|
||||
}
|
||||
await recreateDirectory(outputPath);
|
||||
writeStream.write(
|
||||
`\nCloning Repo Custom ${customGitUrl} to ${outputPath}: ✅\n`,
|
||||
);
|
||||
|
||||
if (customGitSSHKeyId) {
|
||||
await updateSSHKeyById({
|
||||
sshKeyId: customGitSSHKeyId,
|
||||
lastUsedAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
const { port } = sanitizeRepoPathSSH(customGitUrl);
|
||||
const cloneArgs = [
|
||||
"clone",
|
||||
"--branch",
|
||||
customGitBranch,
|
||||
"--depth",
|
||||
"1",
|
||||
...(enableSubmodules ? ["--recurse-submodules"] : []),
|
||||
customGitUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
];
|
||||
|
||||
await spawnAsync(
|
||||
"git",
|
||||
cloneArgs,
|
||||
(data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
},
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
...(customGitSSHKeyId && {
|
||||
GIT_SSH_COMMAND: `ssh -i ${temporalKeyPath}${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`,
|
||||
}),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
writeStream.write(`\nCloned Custom Git ${customGitUrl}: ✅\n`);
|
||||
} catch (error) {
|
||||
writeStream.write(`\nERROR Cloning Custom Git: ${error}: ❌\n`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
command += addHostToKnownHostsCommand(customGitUrl);
|
||||
}
|
||||
};
|
||||
|
||||
export const getCustomGitCloneCommand = async (
|
||||
entity: {
|
||||
appName: string;
|
||||
customGitUrl?: string | null;
|
||||
customGitBranch?: string | null;
|
||||
customGitSSHKeyId?: string | null;
|
||||
serverId: string | null;
|
||||
enableSubmodules: boolean;
|
||||
},
|
||||
logPath: string,
|
||||
isCompose = false,
|
||||
) => {
|
||||
const { SSH_PATH, COMPOSE_PATH, APPLICATIONS_PATH } = paths(true);
|
||||
const {
|
||||
appName,
|
||||
customGitUrl,
|
||||
customGitBranch,
|
||||
customGitSSHKeyId,
|
||||
serverId,
|
||||
enableSubmodules,
|
||||
} = entity;
|
||||
|
||||
if (!customGitUrl || !customGitBranch) {
|
||||
const command = `
|
||||
echo "Error: ❌ Repository not found" >> ${logPath};
|
||||
exit 1;
|
||||
`;
|
||||
|
||||
await execAsyncRemote(serverId, command);
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error: Repository not found",
|
||||
});
|
||||
}
|
||||
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
const knownHostsPath = path.join(SSH_PATH, "known_hosts");
|
||||
command += `rm -rf ${outputPath};`;
|
||||
command += `mkdir -p ${outputPath};`;
|
||||
command += `echo "Cloning Repo Custom ${customGitUrl} to ${outputPath}: ✅";`;
|
||||
|
||||
if (customGitSSHKeyId) {
|
||||
await updateSSHKeyById({
|
||||
@@ -161,48 +67,22 @@ export const getCustomGitCloneCommand = async (
|
||||
lastUsedAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
try {
|
||||
const command = [];
|
||||
if (!isHttpOrHttps(customGitUrl)) {
|
||||
if (!customGitSSHKeyId) {
|
||||
command.push(
|
||||
`echo "Error: you are trying to clone a ssh repository without a ssh key, please set a ssh key ❌" >> ${logPath};
|
||||
exit 1;
|
||||
`,
|
||||
);
|
||||
}
|
||||
command.push(addHostToKnownHostsCommand(customGitUrl));
|
||||
}
|
||||
command.push(`rm -rf ${outputPath};`);
|
||||
command.push(`mkdir -p ${outputPath};`);
|
||||
command.push(
|
||||
`echo "Cloning Custom Git ${customGitUrl}" to ${outputPath}: ✅ >> ${logPath};`,
|
||||
);
|
||||
if (customGitSSHKeyId) {
|
||||
const sshKey = await findSSHKeyById(customGitSSHKeyId);
|
||||
const { port } = sanitizeRepoPathSSH(customGitUrl);
|
||||
const gitSshCommand = `ssh -i /tmp/id_rsa${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`;
|
||||
command.push(
|
||||
`
|
||||
echo "${sshKey.privateKey}" > /tmp/id_rsa
|
||||
chmod 600 /tmp/id_rsa
|
||||
export GIT_SSH_COMMAND="${gitSshCommand}"
|
||||
`,
|
||||
);
|
||||
}
|
||||
|
||||
command.push(
|
||||
`if ! git clone --branch ${customGitBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${customGitUrl} ${outputPath} >> ${logPath} 2>&1; then
|
||||
echo "❌ [ERROR] Fail to clone the repository ${customGitUrl}" >> ${logPath};
|
||||
if (customGitSSHKeyId) {
|
||||
const sshKey = await findSSHKeyById(customGitSSHKeyId);
|
||||
const { port } = sanitizeRepoPathSSH(customGitUrl);
|
||||
const gitSshCommand = `ssh -i /tmp/id_rsa${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`;
|
||||
command += `echo "${sshKey.privateKey}" > /tmp/id_rsa;`;
|
||||
command += "chmod 600 /tmp/id_rsa;";
|
||||
command += `export GIT_SSH_COMMAND="${gitSshCommand}";`;
|
||||
}
|
||||
command += `if ! git clone --branch ${customGitBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${customGitUrl} ${outputPath}; then
|
||||
echo "❌ [ERROR] Fail to clone the repository ${customGitUrl}";
|
||||
exit 1;
|
||||
fi
|
||||
`,
|
||||
);
|
||||
command.push(`echo "Cloned Custom Git ${customGitUrl}: ✅" >> ${logPath};`);
|
||||
return command.join("\n");
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
`;
|
||||
|
||||
return command;
|
||||
};
|
||||
|
||||
const isHttpOrHttps = (url: string): boolean => {
|
||||
@@ -210,19 +90,19 @@ const isHttpOrHttps = (url: string): boolean => {
|
||||
return regex.test(url);
|
||||
};
|
||||
|
||||
const addHostToKnownHosts = async (repositoryURL: string) => {
|
||||
const { SSH_PATH } = paths();
|
||||
const { domain, port } = sanitizeRepoPathSSH(repositoryURL);
|
||||
const knownHostsPath = path.join(SSH_PATH, "known_hosts");
|
||||
// const addHostToKnownHosts = async (repositoryURL: string) => {
|
||||
// const { SSH_PATH } = paths();
|
||||
// const { domain, port } = sanitizeRepoPathSSH(repositoryURL);
|
||||
// const knownHostsPath = path.join(SSH_PATH, "known_hosts");
|
||||
|
||||
const command = `ssh-keyscan -p ${port} ${domain} >> ${knownHostsPath}`;
|
||||
try {
|
||||
await execAsync(command);
|
||||
} catch (error) {
|
||||
console.error(`Error adding host to known_hosts: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
// const command = `ssh-keyscan -p ${port} ${domain} >> ${knownHostsPath}`;
|
||||
// try {
|
||||
// await execAsync(command);
|
||||
// } catch (error) {
|
||||
// console.error(`Error adding host to known_hosts: ${error}`);
|
||||
// throw error;
|
||||
// }
|
||||
// };
|
||||
|
||||
const addHostToKnownHostsCommand = (repositoryURL: string) => {
|
||||
const { SSH_PATH } = paths(true);
|
||||
@@ -267,160 +147,43 @@ const sanitizeRepoPathSSH = (input: string) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const cloneGitRawRepository = async (entity: {
|
||||
interface Props {
|
||||
appName: string;
|
||||
customGitUrl?: string | null;
|
||||
customGitBranch?: string | null;
|
||||
customGitSSHKeyId?: string | null;
|
||||
enableSubmodules?: boolean;
|
||||
}) => {
|
||||
const {
|
||||
appName,
|
||||
customGitUrl,
|
||||
customGitBranch,
|
||||
customGitSSHKeyId,
|
||||
enableSubmodules,
|
||||
} = entity;
|
||||
type?: "application" | "compose";
|
||||
serverId: string | null;
|
||||
}
|
||||
|
||||
if (!customGitUrl || !customGitBranch) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error: Repository not found",
|
||||
});
|
||||
}
|
||||
|
||||
const { SSH_PATH, COMPOSE_PATH } = paths();
|
||||
const temporalKeyPath = path.join("/tmp", "id_rsa");
|
||||
const basePath = COMPOSE_PATH;
|
||||
export const getGitCommitInfo = async ({
|
||||
appName,
|
||||
type = "application",
|
||||
serverId,
|
||||
}: Props) => {
|
||||
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(!!serverId);
|
||||
const basePath = type === "compose" ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
const knownHostsPath = path.join(SSH_PATH, "known_hosts");
|
||||
|
||||
if (customGitSSHKeyId) {
|
||||
const sshKey = await findSSHKeyById(customGitSSHKeyId);
|
||||
|
||||
await execAsync(`
|
||||
echo "${sshKey.privateKey}" > ${temporalKeyPath}
|
||||
chmod 600 ${temporalKeyPath}
|
||||
`);
|
||||
}
|
||||
|
||||
let stdoutResult = "";
|
||||
const result = {
|
||||
message: "",
|
||||
hash: "",
|
||||
};
|
||||
try {
|
||||
if (!isHttpOrHttps(customGitUrl)) {
|
||||
if (!customGitSSHKeyId) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message:
|
||||
"Error: you are trying to clone a ssh repository without a ssh key, please set a ssh key",
|
||||
});
|
||||
}
|
||||
await addHostToKnownHosts(customGitUrl);
|
||||
}
|
||||
await recreateDirectory(outputPath);
|
||||
|
||||
if (customGitSSHKeyId) {
|
||||
await updateSSHKeyById({
|
||||
sshKeyId: customGitSSHKeyId,
|
||||
lastUsedAt: new Date().toISOString(),
|
||||
});
|
||||
const gitCommand = `git -C ${outputPath} log -1 --pretty=format:"%H---DELIMITER---%B"`;
|
||||
if (serverId) {
|
||||
const { stdout } = await execAsyncRemote(serverId, gitCommand);
|
||||
stdoutResult = stdout.trim();
|
||||
} else {
|
||||
const { stdout } = await execAsync(gitCommand);
|
||||
stdoutResult = stdout.trim();
|
||||
}
|
||||
|
||||
const { port } = sanitizeRepoPathSSH(customGitUrl);
|
||||
const cloneArgs = [
|
||||
"clone",
|
||||
"--branch",
|
||||
customGitBranch,
|
||||
"--depth",
|
||||
"1",
|
||||
...(enableSubmodules ? ["--recurse-submodules"] : []),
|
||||
customGitUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
];
|
||||
|
||||
await spawnAsync("git", cloneArgs, (_data) => {}, {
|
||||
env: {
|
||||
...process.env,
|
||||
...(customGitSSHKeyId && {
|
||||
GIT_SSH_COMMAND: `ssh -i ${temporalKeyPath}${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`,
|
||||
}),
|
||||
},
|
||||
});
|
||||
const parts = stdoutResult.split("---DELIMITER---");
|
||||
if (parts && parts.length === 2) {
|
||||
result.hash = parts[0]?.trim() || "";
|
||||
result.message = parts[1]?.trim() || "";
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const cloneRawGitRepositoryRemote = async (compose: Compose) => {
|
||||
const {
|
||||
appName,
|
||||
customGitBranch,
|
||||
customGitUrl,
|
||||
customGitSSHKeyId,
|
||||
serverId,
|
||||
enableSubmodules,
|
||||
} = compose;
|
||||
|
||||
if (!serverId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Server not found",
|
||||
});
|
||||
}
|
||||
if (!customGitUrl) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Git Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
const { SSH_PATH, COMPOSE_PATH } = paths(true);
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
const knownHostsPath = path.join(SSH_PATH, "known_hosts");
|
||||
|
||||
if (customGitSSHKeyId) {
|
||||
await updateSSHKeyById({
|
||||
sshKeyId: customGitSSHKeyId,
|
||||
lastUsedAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
try {
|
||||
const command = [];
|
||||
if (!isHttpOrHttps(customGitUrl)) {
|
||||
if (!customGitSSHKeyId) {
|
||||
command.push(
|
||||
`echo "Error: you are trying to clone a ssh repository without a ssh key, please set a ssh key ❌" ;
|
||||
exit 1;
|
||||
`,
|
||||
);
|
||||
}
|
||||
command.push(addHostToKnownHostsCommand(customGitUrl));
|
||||
}
|
||||
command.push(`rm -rf ${outputPath};`);
|
||||
command.push(`mkdir -p ${outputPath};`);
|
||||
if (customGitSSHKeyId) {
|
||||
const sshKey = await findSSHKeyById(customGitSSHKeyId);
|
||||
const { port } = sanitizeRepoPathSSH(customGitUrl);
|
||||
const gitSshCommand = `ssh -i /tmp/id_rsa${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`;
|
||||
command.push(
|
||||
`
|
||||
echo "${sshKey.privateKey}" > /tmp/id_rsa
|
||||
chmod 600 /tmp/id_rsa
|
||||
export GIT_SSH_COMMAND="${gitSshCommand}"
|
||||
`,
|
||||
);
|
||||
}
|
||||
|
||||
command.push(
|
||||
`if ! git clone --branch ${customGitBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${customGitUrl} ${outputPath} ; then
|
||||
echo "[ERROR] Fail to clone the repository ";
|
||||
exit 1;
|
||||
fi
|
||||
`,
|
||||
);
|
||||
|
||||
await execAsyncRemote(serverId, command.join("\n"));
|
||||
} catch (error) {
|
||||
throw error;
|
||||
console.error(`Error getting git commit info: ${error}`);
|
||||
return null;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import type { Compose } from "@dokploy/server/services/compose";
|
||||
import {
|
||||
findGiteaById,
|
||||
type Gitea,
|
||||
@@ -9,9 +7,6 @@ import {
|
||||
} from "@dokploy/server/services/gitea";
|
||||
import type { InferResultType } from "@dokploy/server/types/with";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { recreateDirectory } from "../filesystem/directory";
|
||||
import { execAsyncRemote } from "../process/execAsync";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
|
||||
export const getErrorCloneRequirements = (entity: {
|
||||
giteaRepository?: string | null;
|
||||
@@ -119,79 +114,27 @@ export type ApplicationWithGitea = InferResultType<
|
||||
|
||||
export type ComposeWithGitea = InferResultType<"compose", { gitea: true }>;
|
||||
|
||||
export const getGiteaCloneCommand = async (
|
||||
entity: ApplicationWithGitea | ComposeWithGitea,
|
||||
logPath: string,
|
||||
isCompose = false,
|
||||
) => {
|
||||
const {
|
||||
appName,
|
||||
giteaBranch,
|
||||
giteaId,
|
||||
giteaOwner,
|
||||
giteaRepository,
|
||||
serverId,
|
||||
enableSubmodules,
|
||||
} = entity;
|
||||
|
||||
if (!serverId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Server not found",
|
||||
});
|
||||
}
|
||||
|
||||
if (!giteaId) {
|
||||
const command = `
|
||||
echo "Error: ❌ Gitlab Provider not found" >> ${logPath};
|
||||
exit 1;
|
||||
`;
|
||||
|
||||
await execAsyncRemote(serverId, command);
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitea Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
// Use paths(true) for remote operations
|
||||
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(true);
|
||||
await refreshGiteaToken(giteaId);
|
||||
const gitea = await findGiteaById(giteaId);
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
|
||||
const repoClone = `${giteaOwner}/${giteaRepository}.git`;
|
||||
const cloneUrl = buildGiteaCloneUrl(
|
||||
gitea?.giteaUrl!,
|
||||
gitea?.accessToken!,
|
||||
giteaOwner!,
|
||||
giteaRepository!,
|
||||
);
|
||||
|
||||
const cloneCommand = `
|
||||
rm -rf ${outputPath};
|
||||
mkdir -p ${outputPath};
|
||||
|
||||
if ! git clone --branch ${giteaBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
|
||||
echo "❌ [ERROR] Failed to clone the repository ${repoClone}" >> ${logPath};
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
echo "Cloned ${repoClone} to ${outputPath}: ✅" >> ${logPath};
|
||||
`;
|
||||
|
||||
return cloneCommand;
|
||||
type GiteaClone = (ApplicationWithGitea | ComposeWithGitea) & {
|
||||
serverId: string | null;
|
||||
type?: "application" | "compose";
|
||||
};
|
||||
|
||||
export const cloneGiteaRepository = async (
|
||||
entity: ApplicationWithGitea | ComposeWithGitea,
|
||||
logPath: string,
|
||||
isCompose = false,
|
||||
) => {
|
||||
const { APPLICATIONS_PATH, COMPOSE_PATH } = paths();
|
||||
interface CloneGiteaRepository {
|
||||
appName: string;
|
||||
giteaBranch: string | null;
|
||||
giteaId: string | null;
|
||||
giteaOwner: string | null;
|
||||
giteaRepository: string | null;
|
||||
enableSubmodules: boolean;
|
||||
serverId: string | null;
|
||||
type?: "application" | "compose";
|
||||
}
|
||||
|
||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||
export const cloneGiteaRepository = async ({
|
||||
type = "application",
|
||||
...entity
|
||||
}: CloneGiteaRepository) => {
|
||||
let command = "set -e;";
|
||||
const {
|
||||
appName,
|
||||
giteaBranch,
|
||||
@@ -199,27 +142,27 @@ export const cloneGiteaRepository = async (
|
||||
giteaOwner,
|
||||
giteaRepository,
|
||||
enableSubmodules,
|
||||
serverId,
|
||||
} = entity;
|
||||
const { APPLICATIONS_PATH, COMPOSE_PATH } = paths(!!serverId);
|
||||
|
||||
if (!giteaId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitea Provider not found",
|
||||
});
|
||||
command += `echo "Error: ❌ Gitea Provider not found"; exit 1;`;
|
||||
return command;
|
||||
}
|
||||
|
||||
await refreshGiteaToken(giteaId);
|
||||
const giteaProvider = await findGiteaById(giteaId);
|
||||
|
||||
if (!giteaProvider) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitea provider not found in the database",
|
||||
});
|
||||
command += `echo "❌ [ERROR] Gitea provider not found in the database"; exit 1;`;
|
||||
return command;
|
||||
}
|
||||
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const basePath = type === "compose" ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
command += `rm -rf ${outputPath};`;
|
||||
command += `mkdir -p ${outputPath};`;
|
||||
|
||||
const repoClone = `${giteaOwner}/${giteaRepository}.git`;
|
||||
const cloneUrl = buildGiteaCloneUrl(
|
||||
@@ -229,134 +172,9 @@ export const cloneGiteaRepository = async (
|
||||
giteaRepository!,
|
||||
);
|
||||
|
||||
writeStream.write(`\nCloning Repo ${repoClone} to ${outputPath}...\n`);
|
||||
|
||||
try {
|
||||
await spawnAsync(
|
||||
"git",
|
||||
[
|
||||
"clone",
|
||||
"--branch",
|
||||
giteaBranch!,
|
||||
"--depth",
|
||||
"1",
|
||||
...(enableSubmodules ? ["--recurse-submodules"] : []),
|
||||
cloneUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
],
|
||||
(data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
},
|
||||
);
|
||||
writeStream.write(`\nCloned ${repoClone}: ✅\n`);
|
||||
} catch (error) {
|
||||
writeStream.write(`ERROR Cloning: ${error}: ❌`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
}
|
||||
};
|
||||
|
||||
export const cloneRawGiteaRepository = async (entity: Compose) => {
|
||||
const {
|
||||
appName,
|
||||
giteaRepository,
|
||||
giteaOwner,
|
||||
giteaBranch,
|
||||
giteaId,
|
||||
enableSubmodules,
|
||||
} = entity;
|
||||
const { COMPOSE_PATH } = paths();
|
||||
|
||||
if (!giteaId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitea Provider not found",
|
||||
});
|
||||
}
|
||||
await refreshGiteaToken(giteaId);
|
||||
const giteaProvider = await findGiteaById(giteaId);
|
||||
if (!giteaProvider) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitea provider not found in the database",
|
||||
});
|
||||
}
|
||||
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
|
||||
const cloneUrl = buildGiteaCloneUrl(
|
||||
giteaProvider.giteaUrl,
|
||||
giteaProvider.accessToken!,
|
||||
giteaOwner!,
|
||||
giteaRepository!,
|
||||
);
|
||||
|
||||
try {
|
||||
await spawnAsync("git", [
|
||||
"clone",
|
||||
"--branch",
|
||||
giteaBranch!,
|
||||
"--depth",
|
||||
"1",
|
||||
...(enableSubmodules ? ["--recurse-submodules"] : []),
|
||||
cloneUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
]);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const cloneRawGiteaRepositoryRemote = async (compose: Compose) => {
|
||||
const {
|
||||
appName,
|
||||
giteaRepository,
|
||||
giteaOwner,
|
||||
giteaBranch,
|
||||
giteaId,
|
||||
serverId,
|
||||
enableSubmodules,
|
||||
} = compose;
|
||||
|
||||
if (!serverId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Server not found",
|
||||
});
|
||||
}
|
||||
if (!giteaId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitea Provider not found",
|
||||
});
|
||||
}
|
||||
const { COMPOSE_PATH } = paths(true);
|
||||
const giteaProvider = await findGiteaById(giteaId);
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
const cloneUrl = buildGiteaCloneUrl(
|
||||
giteaProvider.giteaUrl,
|
||||
giteaProvider.accessToken!,
|
||||
giteaOwner!,
|
||||
giteaRepository!,
|
||||
);
|
||||
|
||||
try {
|
||||
const command = `
|
||||
rm -rf ${outputPath};
|
||||
git clone --branch ${giteaBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath}
|
||||
`;
|
||||
await execAsyncRemote(serverId, command);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
command += `echo "Cloning Repo ${repoClone} to ${outputPath}: ✅";`;
|
||||
command += `git clone --branch ${giteaBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} --progress;`;
|
||||
return command;
|
||||
};
|
||||
|
||||
export const haveGiteaRequirements = (giteaProvider: Gitea) => {
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import type { apiFindGithubBranches } from "@dokploy/server/db/schema";
|
||||
import type { Compose } from "@dokploy/server/services/compose";
|
||||
import { findGithubById, type Github } from "@dokploy/server/services/github";
|
||||
import type { InferResultType } from "@dokploy/server/types/with";
|
||||
import { createAppAuth } from "@octokit/auth-app";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { Octokit } from "octokit";
|
||||
import { recreateDirectory } from "../filesystem/directory";
|
||||
import { execAsyncRemote } from "../process/execAsync";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
|
||||
export const authGithub = (githubProvider: Github): Octokit => {
|
||||
if (!haveGithubRequirements(githubProvider)) {
|
||||
@@ -123,42 +118,39 @@ interface CloneGithubRepository {
|
||||
branch: string | null;
|
||||
githubId: string | null;
|
||||
repository: string | null;
|
||||
logPath: string;
|
||||
type?: "application" | "compose";
|
||||
enableSubmodules: boolean;
|
||||
serverId: string | null;
|
||||
}
|
||||
export const cloneGithubRepository = async ({
|
||||
logPath,
|
||||
type = "application",
|
||||
...entity
|
||||
}: CloneGithubRepository) => {
|
||||
let command = "set -e;";
|
||||
const isCompose = type === "compose";
|
||||
const { APPLICATIONS_PATH, COMPOSE_PATH } = paths();
|
||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||
const { appName, repository, owner, branch, githubId, enableSubmodules } =
|
||||
entity;
|
||||
const {
|
||||
appName,
|
||||
repository,
|
||||
owner,
|
||||
branch,
|
||||
githubId,
|
||||
enableSubmodules,
|
||||
serverId,
|
||||
} = entity;
|
||||
const { APPLICATIONS_PATH, COMPOSE_PATH } = paths(!!serverId);
|
||||
|
||||
if (!githubId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "GitHub Provider not found",
|
||||
});
|
||||
command += `echo "Error: ❌ Github Provider not found"; exit 1;`;
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
const requirements = getErrorCloneRequirements(entity);
|
||||
|
||||
// Check if requirements are met
|
||||
if (requirements.length > 0) {
|
||||
writeStream.write(
|
||||
`\nGitHub Repository configuration failed for application: ${appName}\n`,
|
||||
);
|
||||
writeStream.write("Reasons:\n");
|
||||
writeStream.write(requirements.join("\n"));
|
||||
writeStream.end();
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error: GitHub repository information is incomplete.",
|
||||
});
|
||||
command += `echo "GitHub Repository configuration failed for application: ${appName}"; echo "Reasons:"; echo "${requirements.join("\n")}"; exit 1;`;
|
||||
return command;
|
||||
}
|
||||
|
||||
const githubProvider = await findGithubById(githubId);
|
||||
@@ -167,193 +159,15 @@ export const cloneGithubRepository = async ({
|
||||
const octokit = authGithub(githubProvider);
|
||||
const token = await getGithubToken(octokit);
|
||||
const repoclone = `github.com/${owner}/${repository}.git`;
|
||||
await recreateDirectory(outputPath);
|
||||
// await recreateDirectory(outputPath);
|
||||
command += `rm -rf ${outputPath};`;
|
||||
command += `mkdir -p ${outputPath};`;
|
||||
const cloneUrl = `https://oauth2:${token}@${repoclone}`;
|
||||
|
||||
try {
|
||||
writeStream.write(`\nCloning Repo ${repoclone} to ${outputPath}: ✅\n`);
|
||||
const cloneArgs = [
|
||||
"clone",
|
||||
"--branch",
|
||||
branch!,
|
||||
"--depth",
|
||||
"1",
|
||||
...(enableSubmodules ? ["--recurse-submodules"] : []),
|
||||
cloneUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
];
|
||||
command += `echo "Cloning Repo ${repoclone} to ${outputPath}: ✅";`;
|
||||
command += `git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} --progress;`;
|
||||
|
||||
await spawnAsync("git", cloneArgs, (data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
});
|
||||
writeStream.write(`\nCloned ${repoclone}: ✅\n`);
|
||||
} catch (error) {
|
||||
writeStream.write(`ERROR Cloning: ${error}: ❌`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
}
|
||||
};
|
||||
|
||||
export const getGithubCloneCommand = async ({
|
||||
logPath,
|
||||
type = "application",
|
||||
...entity
|
||||
}: CloneGithubRepository & { serverId: string }) => {
|
||||
const {
|
||||
appName,
|
||||
repository,
|
||||
owner,
|
||||
branch,
|
||||
githubId,
|
||||
serverId,
|
||||
enableSubmodules,
|
||||
} = entity;
|
||||
const isCompose = type === "compose";
|
||||
if (!serverId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Server not found",
|
||||
});
|
||||
}
|
||||
|
||||
if (!githubId) {
|
||||
const command = `
|
||||
echo "Error: ❌ Github Provider not found" >> ${logPath};
|
||||
exit 1;
|
||||
`;
|
||||
|
||||
await execAsyncRemote(serverId, command);
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "GitHub Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
const requirements = getErrorCloneRequirements(entity);
|
||||
|
||||
// Build log messages
|
||||
let logMessages = "";
|
||||
if (requirements.length > 0) {
|
||||
logMessages += `\nGitHub Repository configuration failed for application: ${appName}\n`;
|
||||
logMessages += "Reasons:\n";
|
||||
logMessages += requirements.join("\n");
|
||||
const escapedLogMessages = logMessages
|
||||
.replace(/\\/g, "\\\\")
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/\n/g, "\\n");
|
||||
|
||||
const bashCommand = `
|
||||
echo "${escapedLogMessages}" >> ${logPath};
|
||||
exit 1; # Exit with error code
|
||||
`;
|
||||
|
||||
await execAsyncRemote(serverId, bashCommand);
|
||||
return;
|
||||
}
|
||||
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(true);
|
||||
const githubProvider = await findGithubById(githubId);
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
const octokit = authGithub(githubProvider);
|
||||
const token = await getGithubToken(octokit);
|
||||
const repoclone = `github.com/${owner}/${repository}.git`;
|
||||
const cloneUrl = `https://oauth2:${token}@${repoclone}`;
|
||||
|
||||
const cloneCommand = `
|
||||
rm -rf ${outputPath};
|
||||
mkdir -p ${outputPath};
|
||||
if ! git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
|
||||
echo "❌ [ERROR] Fail to clone repository ${repoclone}" >> ${logPath};
|
||||
exit 1;
|
||||
fi
|
||||
echo "Cloned ${repoclone} to ${outputPath}: ✅" >> ${logPath};
|
||||
`;
|
||||
|
||||
return cloneCommand;
|
||||
};
|
||||
|
||||
export const cloneRawGithubRepository = async (entity: Compose) => {
|
||||
const { appName, repository, owner, branch, githubId, enableSubmodules } =
|
||||
entity;
|
||||
|
||||
if (!githubId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "GitHub Provider not found",
|
||||
});
|
||||
}
|
||||
const { COMPOSE_PATH } = paths();
|
||||
const githubProvider = await findGithubById(githubId);
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
const octokit = authGithub(githubProvider);
|
||||
const token = await getGithubToken(octokit);
|
||||
const repoclone = `github.com/${owner}/${repository}.git`;
|
||||
await recreateDirectory(outputPath);
|
||||
const cloneUrl = `https://oauth2:${token}@${repoclone}`;
|
||||
try {
|
||||
const cloneArgs = [
|
||||
"clone",
|
||||
"--branch",
|
||||
branch!,
|
||||
"--depth",
|
||||
"1",
|
||||
...(enableSubmodules ? ["--recurse-submodules"] : []),
|
||||
cloneUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
];
|
||||
await spawnAsync("git", cloneArgs);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const cloneRawGithubRepositoryRemote = async (compose: Compose) => {
|
||||
const {
|
||||
appName,
|
||||
repository,
|
||||
owner,
|
||||
branch,
|
||||
githubId,
|
||||
serverId,
|
||||
enableSubmodules,
|
||||
} = compose;
|
||||
|
||||
if (!serverId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Server not found",
|
||||
});
|
||||
}
|
||||
if (!githubId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "GitHub Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
const { COMPOSE_PATH } = paths(true);
|
||||
const githubProvider = await findGithubById(githubId);
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
const octokit = authGithub(githubProvider);
|
||||
const token = await getGithubToken(octokit);
|
||||
const repoclone = `github.com/${owner}/${repository}.git`;
|
||||
const cloneUrl = `https://oauth2:${token}@${repoclone}`;
|
||||
try {
|
||||
const command = `
|
||||
rm -rf ${outputPath};
|
||||
git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath}
|
||||
`;
|
||||
await execAsyncRemote(serverId, command);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
return command;
|
||||
};
|
||||
|
||||
export const getGithubRepositories = async (githubId?: string) => {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import type { apiGitlabTestConnection } from "@dokploy/server/db/schema";
|
||||
import type { Compose } from "@dokploy/server/services/compose";
|
||||
import {
|
||||
findGitlabById,
|
||||
type Gitlab,
|
||||
@@ -10,9 +8,6 @@ import {
|
||||
} from "@dokploy/server/services/gitlab";
|
||||
import type { InferResultType } from "@dokploy/server/types/with";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { recreateDirectory } from "../filesystem/directory";
|
||||
import { execAsyncRemote } from "../process/execAsync";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
|
||||
export const refreshGitlabToken = async (gitlabProviderId: string) => {
|
||||
const gitlabProvider = await findGitlabById(gitlabProviderId);
|
||||
@@ -102,25 +97,34 @@ const getGitlabCloneUrl = (gitlab: GitlabInfo, repoClone: string) => {
|
||||
return cloneUrl;
|
||||
};
|
||||
|
||||
export const cloneGitlabRepository = async (
|
||||
entity: ApplicationWithGitlab | ComposeWithGitlab,
|
||||
logPath: string,
|
||||
isCompose = false,
|
||||
) => {
|
||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||
interface CloneGitlabRepository {
|
||||
appName: string;
|
||||
gitlabBranch: string | null;
|
||||
gitlabId: string | null;
|
||||
gitlabPathNamespace: string | null;
|
||||
enableSubmodules: boolean;
|
||||
serverId: string | null;
|
||||
type?: "application" | "compose";
|
||||
}
|
||||
|
||||
export const cloneGitlabRepository = async ({
|
||||
type = "application",
|
||||
...entity
|
||||
}: CloneGitlabRepository) => {
|
||||
let command = "set -e;";
|
||||
const {
|
||||
appName,
|
||||
gitlabBranch,
|
||||
gitlabId,
|
||||
gitlabPathNamespace,
|
||||
enableSubmodules,
|
||||
serverId,
|
||||
} = entity;
|
||||
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(!!serverId);
|
||||
|
||||
if (!gitlabId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitlab Provider not found",
|
||||
});
|
||||
command += `echo "Error: ❌ Gitlab Provider not found"; exit 1;`;
|
||||
return command;
|
||||
}
|
||||
|
||||
await refreshGitlabToken(gitlabId);
|
||||
@@ -130,127 +134,19 @@ export const cloneGitlabRepository = async (
|
||||
|
||||
// Check if requirements are met
|
||||
if (requirements.length > 0) {
|
||||
writeStream.write(
|
||||
`\nGitLab Repository configuration failed for application: ${appName}\n`,
|
||||
);
|
||||
writeStream.write("Reasons:\n");
|
||||
writeStream.write(requirements.join("\n"));
|
||||
writeStream.end();
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error: GitLab repository information is incomplete.",
|
||||
});
|
||||
command += `echo "❌ [ERROR] GitLab Repository configuration failed for application: ${appName}"; echo "Reasons:"; echo "${requirements.join("\n")}"; exit 1;`;
|
||||
return command;
|
||||
}
|
||||
|
||||
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths();
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const basePath = type === "compose" ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
command += `rm -rf ${outputPath};`;
|
||||
command += `mkdir -p ${outputPath};`;
|
||||
const repoClone = getGitlabRepoClone(gitlab, gitlabPathNamespace);
|
||||
const cloneUrl = getGitlabCloneUrl(gitlab, repoClone);
|
||||
try {
|
||||
writeStream.write(`\nCloning Repo ${repoClone} to ${outputPath}: ✅\n`);
|
||||
const cloneArgs = [
|
||||
"clone",
|
||||
"--branch",
|
||||
gitlabBranch!,
|
||||
"--depth",
|
||||
"1",
|
||||
...(enableSubmodules ? ["--recurse-submodules"] : []),
|
||||
cloneUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
];
|
||||
|
||||
await spawnAsync("git", cloneArgs, (data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
});
|
||||
writeStream.write(`\nCloned ${repoClone}: ✅\n`);
|
||||
} catch (error) {
|
||||
writeStream.write(`ERROR Cloning: ${error}: ❌`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
}
|
||||
};
|
||||
|
||||
export const getGitlabCloneCommand = async (
|
||||
entity: ApplicationWithGitlab | ComposeWithGitlab,
|
||||
logPath: string,
|
||||
isCompose = false,
|
||||
) => {
|
||||
const {
|
||||
appName,
|
||||
gitlabPathNamespace,
|
||||
gitlabBranch,
|
||||
gitlabId,
|
||||
serverId,
|
||||
enableSubmodules,
|
||||
} = entity;
|
||||
|
||||
if (!serverId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Server not found",
|
||||
});
|
||||
}
|
||||
|
||||
if (!gitlabId) {
|
||||
const command = `
|
||||
echo "Error: ❌ Gitlab Provider not found" >> ${logPath};
|
||||
exit 1;
|
||||
`;
|
||||
|
||||
await execAsyncRemote(serverId, command);
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitlab Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
const requirements = getErrorCloneRequirements(entity);
|
||||
|
||||
// Build log messages
|
||||
let logMessages = "";
|
||||
if (requirements.length > 0) {
|
||||
logMessages += `\nGitLab Repository configuration failed for application: ${appName}\n`;
|
||||
logMessages += "Reasons:\n";
|
||||
logMessages += requirements.join("\n");
|
||||
const escapedLogMessages = logMessages
|
||||
.replace(/\\/g, "\\\\")
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/\n/g, "\\n");
|
||||
|
||||
const bashCommand = `
|
||||
echo "${escapedLogMessages}" >> ${logPath};
|
||||
exit 1; # Exit with error code
|
||||
`;
|
||||
|
||||
await execAsyncRemote(serverId, bashCommand);
|
||||
return;
|
||||
}
|
||||
|
||||
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(true);
|
||||
await refreshGitlabToken(gitlabId);
|
||||
const gitlab = await findGitlabById(gitlabId);
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
const repoClone = getGitlabRepoClone(gitlab, gitlabPathNamespace);
|
||||
const cloneUrl = getGitlabCloneUrl(gitlab, repoClone);
|
||||
const cloneCommand = `
|
||||
rm -rf ${outputPath};
|
||||
mkdir -p ${outputPath};
|
||||
if ! git clone --branch ${gitlabBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
|
||||
echo "❌ [ERROR] Fail to clone the repository ${repoClone}" >> ${logPath};
|
||||
exit 1;
|
||||
fi
|
||||
echo "Cloned ${repoClone} to ${outputPath}: ✅" >> ${logPath};
|
||||
`;
|
||||
|
||||
return cloneCommand;
|
||||
command += `echo "Cloning Repo ${repoClone} to ${outputPath}: ✅";`;
|
||||
command += `git clone --branch ${gitlabBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} --progress;`;
|
||||
return command;
|
||||
};
|
||||
|
||||
export const getGitlabRepositories = async (gitlabId?: string) => {
|
||||
@@ -355,88 +251,6 @@ export const getGitlabBranches = async (input: {
|
||||
}[];
|
||||
};
|
||||
|
||||
export const cloneRawGitlabRepository = async (entity: Compose) => {
|
||||
const {
|
||||
appName,
|
||||
gitlabBranch,
|
||||
gitlabId,
|
||||
gitlabPathNamespace,
|
||||
enableSubmodules,
|
||||
} = entity;
|
||||
|
||||
if (!gitlabId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitlab Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
const { COMPOSE_PATH } = paths();
|
||||
await refreshGitlabToken(gitlabId);
|
||||
const gitlabProvider = await findGitlabById(gitlabId);
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
const repoClone = getGitlabRepoClone(gitlabProvider, gitlabPathNamespace);
|
||||
const cloneUrl = getGitlabCloneUrl(gitlabProvider, repoClone);
|
||||
try {
|
||||
const cloneArgs = [
|
||||
"clone",
|
||||
"--branch",
|
||||
gitlabBranch!,
|
||||
"--depth",
|
||||
"1",
|
||||
...(enableSubmodules ? ["--recurse-submodules"] : []),
|
||||
cloneUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
];
|
||||
await spawnAsync("git", cloneArgs);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const cloneRawGitlabRepositoryRemote = async (compose: Compose) => {
|
||||
const {
|
||||
appName,
|
||||
gitlabPathNamespace,
|
||||
gitlabBranch,
|
||||
gitlabId,
|
||||
serverId,
|
||||
enableSubmodules,
|
||||
} = compose;
|
||||
|
||||
if (!serverId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Server not found",
|
||||
});
|
||||
}
|
||||
if (!gitlabId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitlab Provider not found",
|
||||
});
|
||||
}
|
||||
const { COMPOSE_PATH } = paths(true);
|
||||
await refreshGitlabToken(gitlabId);
|
||||
const gitlabProvider = await findGitlabById(gitlabId);
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
const repoClone = getGitlabRepoClone(gitlabProvider, gitlabPathNamespace);
|
||||
const cloneUrl = getGitlabCloneUrl(gitlabProvider, repoClone);
|
||||
try {
|
||||
const command = `
|
||||
rm -rf ${outputPath};
|
||||
git clone --branch ${gitlabBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath}
|
||||
`;
|
||||
await execAsyncRemote(serverId, command);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const testGitlabConnection = async (
|
||||
input: typeof apiGitlabTestConnection._type,
|
||||
) => {
|
||||
|
||||
@@ -1,40 +1,10 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import type { Compose } from "@dokploy/server/services/compose";
|
||||
import { encodeBase64 } from "../docker/utils";
|
||||
import { recreateDirectory } from "../filesystem/directory";
|
||||
import { execAsyncRemote } from "../process/execAsync";
|
||||
|
||||
export const createComposeFile = async (compose: Compose, logPath: string) => {
|
||||
const { COMPOSE_PATH } = paths();
|
||||
const { appName, composeFile } = compose;
|
||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||
const outputPath = join(COMPOSE_PATH, appName, "code");
|
||||
|
||||
try {
|
||||
await recreateDirectory(outputPath);
|
||||
writeStream.write(
|
||||
`\nCreating File 'docker-compose.yml' to ${outputPath}: ✅\n`,
|
||||
);
|
||||
|
||||
await writeFile(join(outputPath, "docker-compose.yml"), composeFile);
|
||||
|
||||
writeStream.write(`\nFile 'docker-compose.yml' created: ✅\n`);
|
||||
} catch (error) {
|
||||
writeStream.write(`\nERROR Creating Compose File: ${error}: ❌\n`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
}
|
||||
};
|
||||
|
||||
export const getCreateComposeFileCommand = (
|
||||
compose: Compose,
|
||||
logPath: string,
|
||||
) => {
|
||||
const { COMPOSE_PATH } = paths(true);
|
||||
export const getCreateComposeFileCommand = (compose: Compose) => {
|
||||
const { COMPOSE_PATH } = paths(!!compose.serverId);
|
||||
const { appName, composeFile } = compose;
|
||||
const outputPath = join(COMPOSE_PATH, appName, "code");
|
||||
const filePath = join(outputPath, "docker-compose.yml");
|
||||
@@ -43,39 +13,7 @@ export const getCreateComposeFileCommand = (
|
||||
rm -rf ${outputPath};
|
||||
mkdir -p ${outputPath};
|
||||
echo "${encodedContent}" | base64 -d > "${filePath}";
|
||||
echo "File 'docker-compose.yml' created: ✅" >> ${logPath};
|
||||
echo "File 'docker-compose.yml' created: ✅";
|
||||
`;
|
||||
return bashCommand;
|
||||
};
|
||||
|
||||
export const createComposeFileRaw = async (compose: Compose) => {
|
||||
const { COMPOSE_PATH } = paths();
|
||||
const { appName, composeFile } = compose;
|
||||
const outputPath = join(COMPOSE_PATH, appName, "code");
|
||||
const filePath = join(outputPath, "docker-compose.yml");
|
||||
try {
|
||||
await recreateDirectory(outputPath);
|
||||
await writeFile(filePath, composeFile);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const createComposeFileRawRemote = async (compose: Compose) => {
|
||||
const { COMPOSE_PATH } = paths(true);
|
||||
const { appName, composeFile, serverId } = compose;
|
||||
const outputPath = join(COMPOSE_PATH, appName, "code");
|
||||
const filePath = join(outputPath, "docker-compose.yml");
|
||||
|
||||
try {
|
||||
const encodedContent = encodeBase64(composeFile);
|
||||
const command = `
|
||||
rm -rf ${outputPath};
|
||||
mkdir -p ${outputPath};
|
||||
echo "${encodedContent}" | base64 -d > "${filePath}";
|
||||
`;
|
||||
await execAsyncRemote(serverId, command);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
26
pnpm-lock.yaml
generated
26
pnpm-lock.yaml
generated
@@ -347,8 +347,8 @@ importers:
|
||||
specifier: ^0.2.1
|
||||
version: 0.2.1(next@15.3.2(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
node-os-utils:
|
||||
specifier: 1.3.7
|
||||
version: 1.3.7
|
||||
specifier: 2.0.1
|
||||
version: 2.0.1
|
||||
node-pty:
|
||||
specifier: 1.0.0
|
||||
version: 1.0.0
|
||||
@@ -473,9 +473,6 @@ importers:
|
||||
'@types/node':
|
||||
specifier: ^18.19.104
|
||||
version: 18.19.104
|
||||
'@types/node-os-utils':
|
||||
specifier: 1.3.4
|
||||
version: 1.3.4
|
||||
'@types/node-schedule':
|
||||
specifier: 2.1.6
|
||||
version: 2.1.6
|
||||
@@ -688,8 +685,8 @@ importers:
|
||||
specifier: 3.3.11
|
||||
version: 3.3.11
|
||||
node-os-utils:
|
||||
specifier: 1.3.7
|
||||
version: 1.3.7
|
||||
specifier: 2.0.1
|
||||
version: 2.0.1
|
||||
node-pty:
|
||||
specifier: 1.0.0
|
||||
version: 1.0.0
|
||||
@@ -766,9 +763,6 @@ importers:
|
||||
'@types/node':
|
||||
specifier: ^18.19.104
|
||||
version: 18.19.104
|
||||
'@types/node-os-utils':
|
||||
specifier: 1.3.4
|
||||
version: 1.3.4
|
||||
'@types/node-schedule':
|
||||
specifier: 2.1.6
|
||||
version: 2.1.6
|
||||
@@ -4000,9 +3994,6 @@ packages:
|
||||
'@types/mysql@2.15.26':
|
||||
resolution: {integrity: sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==}
|
||||
|
||||
'@types/node-os-utils@1.3.4':
|
||||
resolution: {integrity: sha512-BCUYrbdoO4FUbx6MB9atLNFnkxdliFaxdiTJMIPPiecXIApc5zf4NIqV5G1jWv/ReZvtYyHLs40RkBjHX+vykA==}
|
||||
|
||||
'@types/node-schedule@2.1.6':
|
||||
resolution: {integrity: sha512-6AlZSUiNTdaVmH5jXYxX9YgmF1zfOlbjUqw0EllTBmZCnN1R5RR/m/u3No1OiWR05bnQ4jM4/+w4FcGvkAtnKQ==}
|
||||
|
||||
@@ -6312,8 +6303,9 @@ packages:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
node-os-utils@1.3.7:
|
||||
resolution: {integrity: sha512-fvnX9tZbR7WfCG5BAy3yO/nCLyjVWD6MghEq0z5FDfN+ZXpLWNITBdbifxQkQ25ebr16G0N7eRWJisOcMEHG3Q==}
|
||||
node-os-utils@2.0.1:
|
||||
resolution: {integrity: sha512-rH2N3qHZETLhdgTGhMMCE8zU3gsWO4we1MFtrSiAI7tYWrnJRc6dk2PseV4co3Lb0v/MbRONLQI2biHQYbpTpg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
node-pty@1.0.0:
|
||||
resolution: {integrity: sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==}
|
||||
@@ -11338,8 +11330,6 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/node': 20.17.51
|
||||
|
||||
'@types/node-os-utils@1.3.4': {}
|
||||
|
||||
'@types/node-schedule@2.1.6':
|
||||
dependencies:
|
||||
'@types/node': 20.17.51
|
||||
@@ -13852,7 +13842,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/node': 18.19.104
|
||||
|
||||
node-os-utils@1.3.7: {}
|
||||
node-os-utils@2.0.1: {}
|
||||
|
||||
node-pty@1.0.0:
|
||||
dependencies:
|
||||
|
||||
Reference in New Issue
Block a user