mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-07 15:05:23 +02:00
Compare commits
87 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82cfe06fa4 | ||
|
|
19a01665ae | ||
|
|
398300f729 | ||
|
|
605de97805 | ||
|
|
6ba35057ac | ||
|
|
46d1809f84 | ||
|
|
ba5e7e2026 | ||
|
|
8a741e41bb | ||
|
|
1581defc39 | ||
|
|
f5891b8793 | ||
|
|
19244a2dea | ||
|
|
c4c1930195 | ||
|
|
201cc65b09 | ||
|
|
3618be65fc | ||
|
|
e9b4245625 | ||
|
|
e60c68dbeb | ||
|
|
f46444e039 | ||
|
|
05e3d241f1 | ||
|
|
5c2bae2f21 | ||
|
|
d854979fe3 | ||
|
|
8016708798 | ||
|
|
09a98a29e0 | ||
|
|
a4caa47e10 | ||
|
|
969147cd59 | ||
|
|
6369012389 | ||
|
|
69b7777db4 | ||
|
|
b9324e6320 | ||
|
|
04a1a84077 | ||
|
|
735b70b7fe | ||
|
|
61d9ae397a | ||
|
|
ea5d86e295 | ||
|
|
dd06c7006d | ||
|
|
4d36741e50 | ||
|
|
a9b9dd4b66 | ||
|
|
fbb1f1f266 | ||
|
|
c35fe0d457 | ||
|
|
ec081b6f2e | ||
|
|
4518ea2092 | ||
|
|
d549aa6a62 | ||
|
|
62474c1222 | ||
|
|
26ff4075df | ||
|
|
22f704dd59 | ||
|
|
d22aa0583c | ||
|
|
70bb32c590 | ||
|
|
843313ddb9 | ||
|
|
b202974a7d | ||
|
|
c56ddf3ec1 | ||
|
|
b814bdc612 | ||
|
|
d8ab7a59ff | ||
|
|
f718ab334e | ||
|
|
668aaf9a91 | ||
|
|
ef10996dd8 | ||
|
|
a05b75fc67 | ||
|
|
f96114ad80 | ||
|
|
5ac32f9f24 | ||
|
|
7b398939f7 | ||
|
|
fd8f0e8f1f | ||
|
|
4f2268e66f | ||
|
|
b99d532582 | ||
|
|
fb2bb99a2c | ||
|
|
785172fa7b | ||
|
|
43701915f1 | ||
|
|
2619733915 | ||
|
|
8aa496b773 | ||
|
|
1ce153371a | ||
|
|
41849654a7 | ||
|
|
a475361b80 | ||
|
|
1dc5bbd9bd | ||
|
|
d55e934978 | ||
|
|
dddb866233 | ||
|
|
0b58092c8a | ||
|
|
759955e05e | ||
|
|
5949005458 | ||
|
|
71b550f7e6 | ||
|
|
832a98734a | ||
|
|
65b3ce831f | ||
|
|
6613cb7587 | ||
|
|
75a43896a2 | ||
|
|
64e48a7bbe | ||
|
|
5434d9730d | ||
|
|
373c78a927 | ||
|
|
53b66e41e2 | ||
|
|
0f100c7bc8 | ||
|
|
856b6ceec6 | ||
|
|
a14cc09933 | ||
|
|
94c00312c1 | ||
|
|
6da122eab7 |
6
.github/pull_request_template.md
vendored
6
.github/pull_request_template.md
vendored
@@ -6,9 +6,9 @@ Please describe in a short paragraph what this PR is about.
|
|||||||
|
|
||||||
Before submitting this PR, please make sure that:
|
Before submitting this PR, please make sure that:
|
||||||
|
|
||||||
- [] You created a dedicated branch based on the `canary` branch.
|
- [ ] You created a dedicated branch based on the `canary` branch.
|
||||||
- [] You have read the suggestions in the CONTRIBUTING.md file https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#pull-request
|
- [ ] You have read the suggestions in the CONTRIBUTING.md file https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#pull-request
|
||||||
- [] You have tested this PR in your local instance.
|
- [ ] You have tested this PR in your local instance.
|
||||||
|
|
||||||
## Issues related (if applicable)
|
## Issues related (if applicable)
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
deployRemoteApplication,
|
deployApplication,
|
||||||
deployRemoteCompose,
|
deployCompose,
|
||||||
deployRemotePreviewApplication,
|
deployPreviewApplication,
|
||||||
rebuildRemoteApplication,
|
rebuildApplication,
|
||||||
rebuildRemoteCompose,
|
rebuildCompose,
|
||||||
updateApplicationStatus,
|
updateApplicationStatus,
|
||||||
updateCompose,
|
updateCompose,
|
||||||
updatePreviewDeployment,
|
updatePreviewDeployment,
|
||||||
@@ -16,13 +16,13 @@ export const deploy = async (job: DeployJob) => {
|
|||||||
await updateApplicationStatus(job.applicationId, "running");
|
await updateApplicationStatus(job.applicationId, "running");
|
||||||
if (job.server) {
|
if (job.server) {
|
||||||
if (job.type === "redeploy") {
|
if (job.type === "redeploy") {
|
||||||
await rebuildRemoteApplication({
|
await rebuildApplication({
|
||||||
applicationId: job.applicationId,
|
applicationId: job.applicationId,
|
||||||
titleLog: job.titleLog || "Rebuild deployment",
|
titleLog: job.titleLog || "Rebuild deployment",
|
||||||
descriptionLog: job.descriptionLog || "",
|
descriptionLog: job.descriptionLog || "",
|
||||||
});
|
});
|
||||||
} else if (job.type === "deploy") {
|
} else if (job.type === "deploy") {
|
||||||
await deployRemoteApplication({
|
await deployApplication({
|
||||||
applicationId: job.applicationId,
|
applicationId: job.applicationId,
|
||||||
titleLog: job.titleLog || "Manual deployment",
|
titleLog: job.titleLog || "Manual deployment",
|
||||||
descriptionLog: job.descriptionLog || "",
|
descriptionLog: job.descriptionLog || "",
|
||||||
@@ -36,13 +36,13 @@ export const deploy = async (job: DeployJob) => {
|
|||||||
|
|
||||||
if (job.server) {
|
if (job.server) {
|
||||||
if (job.type === "redeploy") {
|
if (job.type === "redeploy") {
|
||||||
await rebuildRemoteCompose({
|
await rebuildCompose({
|
||||||
composeId: job.composeId,
|
composeId: job.composeId,
|
||||||
titleLog: job.titleLog || "Rebuild deployment",
|
titleLog: job.titleLog || "Rebuild deployment",
|
||||||
descriptionLog: job.descriptionLog || "",
|
descriptionLog: job.descriptionLog || "",
|
||||||
});
|
});
|
||||||
} else if (job.type === "deploy") {
|
} else if (job.type === "deploy") {
|
||||||
await deployRemoteCompose({
|
await deployCompose({
|
||||||
composeId: job.composeId,
|
composeId: job.composeId,
|
||||||
titleLog: job.titleLog || "Manual deployment",
|
titleLog: job.titleLog || "Manual deployment",
|
||||||
descriptionLog: job.descriptionLog || "",
|
descriptionLog: job.descriptionLog || "",
|
||||||
@@ -55,7 +55,7 @@ export const deploy = async (job: DeployJob) => {
|
|||||||
});
|
});
|
||||||
if (job.server) {
|
if (job.server) {
|
||||||
if (job.type === "deploy") {
|
if (job.type === "deploy") {
|
||||||
await deployRemotePreviewApplication({
|
await deployPreviewApplication({
|
||||||
applicationId: job.applicationId,
|
applicationId: job.applicationId,
|
||||||
titleLog: job.titleLog || "Preview Deployment",
|
titleLog: job.titleLog || "Preview Deployment",
|
||||||
descriptionLog: job.descriptionLog || "",
|
descriptionLog: job.descriptionLog || "",
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
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", () => {
|
describe("GitHub Webhook Skip CI", () => {
|
||||||
const mockGithubHeaders = {
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ const baseApp: ApplicationNested = {
|
|||||||
triggerType: "push",
|
triggerType: "push",
|
||||||
appName: "",
|
appName: "",
|
||||||
autoDeploy: true,
|
autoDeploy: true,
|
||||||
|
endpointSpecSwarm: null,
|
||||||
serverId: "",
|
serverId: "",
|
||||||
registryUrl: "",
|
registryUrl: "",
|
||||||
branch: null,
|
branch: null,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const baseApp: ApplicationNested = {
|
|||||||
giteaId: "",
|
giteaId: "",
|
||||||
cleanCache: false,
|
cleanCache: false,
|
||||||
applicationStatus: "done",
|
applicationStatus: "done",
|
||||||
|
endpointSpecSwarm: null,
|
||||||
appName: "",
|
appName: "",
|
||||||
autoDeploy: true,
|
autoDeploy: true,
|
||||||
enableSubmodules: false,
|
enableSubmodules: false,
|
||||||
|
|||||||
@@ -122,6 +122,22 @@ const NetworkSwarmSchema = z.array(
|
|||||||
|
|
||||||
const LabelsSwarmSchema = z.record(z.string());
|
const LabelsSwarmSchema = z.record(z.string());
|
||||||
|
|
||||||
|
const EndpointPortConfigSwarmSchema = z
|
||||||
|
.object({
|
||||||
|
Protocol: z.string().optional(),
|
||||||
|
TargetPort: z.number().optional(),
|
||||||
|
PublishedPort: z.number().optional(),
|
||||||
|
PublishMode: z.string().optional(),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
const EndpointSpecSwarmSchema = z
|
||||||
|
.object({
|
||||||
|
Mode: z.string().optional(),
|
||||||
|
Ports: z.array(EndpointPortConfigSwarmSchema).optional(),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
const createStringToJSONSchema = (schema: z.ZodTypeAny) => {
|
const createStringToJSONSchema = (schema: z.ZodTypeAny) => {
|
||||||
return z
|
return z
|
||||||
.string()
|
.string()
|
||||||
@@ -178,6 +194,9 @@ const addSwarmSettings = z.object({
|
|||||||
labelsSwarm: createStringToJSONSchema(LabelsSwarmSchema).nullable(),
|
labelsSwarm: createStringToJSONSchema(LabelsSwarmSchema).nullable(),
|
||||||
networkSwarm: createStringToJSONSchema(NetworkSwarmSchema).nullable(),
|
networkSwarm: createStringToJSONSchema(NetworkSwarmSchema).nullable(),
|
||||||
stopGracePeriodSwarm: z.bigint().nullable(),
|
stopGracePeriodSwarm: z.bigint().nullable(),
|
||||||
|
endpointSpecSwarm: createStringToJSONSchema(
|
||||||
|
EndpointSpecSwarmSchema,
|
||||||
|
).nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type AddSwarmSettings = z.infer<typeof addSwarmSettings>;
|
type AddSwarmSettings = z.infer<typeof addSwarmSettings>;
|
||||||
@@ -234,6 +253,7 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
|
|||||||
labelsSwarm: null,
|
labelsSwarm: null,
|
||||||
networkSwarm: null,
|
networkSwarm: null,
|
||||||
stopGracePeriodSwarm: null,
|
stopGracePeriodSwarm: null,
|
||||||
|
endpointSpecSwarm: null,
|
||||||
},
|
},
|
||||||
resolver: zodResolver(addSwarmSettings),
|
resolver: zodResolver(addSwarmSettings),
|
||||||
});
|
});
|
||||||
@@ -275,6 +295,9 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
|
|||||||
? JSON.stringify(data.networkSwarm, null, 2)
|
? JSON.stringify(data.networkSwarm, null, 2)
|
||||||
: null,
|
: null,
|
||||||
stopGracePeriodSwarm: normalizedStopGracePeriod,
|
stopGracePeriodSwarm: normalizedStopGracePeriod,
|
||||||
|
endpointSpecSwarm: data.endpointSpecSwarm
|
||||||
|
? JSON.stringify(data.endpointSpecSwarm, null, 2)
|
||||||
|
: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form, form.reset, data]);
|
}, [form, form.reset, data]);
|
||||||
@@ -296,6 +319,7 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
|
|||||||
labelsSwarm: data.labelsSwarm,
|
labelsSwarm: data.labelsSwarm,
|
||||||
networkSwarm: data.networkSwarm,
|
networkSwarm: data.networkSwarm,
|
||||||
stopGracePeriodSwarm: data.stopGracePeriodSwarm ?? null,
|
stopGracePeriodSwarm: data.stopGracePeriodSwarm ?? null,
|
||||||
|
endpointSpecSwarm: data.endpointSpecSwarm,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Swarm settings updated");
|
toast.success("Swarm settings updated");
|
||||||
@@ -846,6 +870,67 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="endpointSpecSwarm"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="relative ">
|
||||||
|
<FormLabel>Endpoint Spec</FormLabel>
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<FormDescription className="break-all w-fit flex flex-row gap-1 items-center">
|
||||||
|
Check the interface
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground" />
|
||||||
|
</FormDescription>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
className="w-full z-[999]"
|
||||||
|
align="start"
|
||||||
|
side="bottom"
|
||||||
|
>
|
||||||
|
<code>
|
||||||
|
<pre>
|
||||||
|
{`{
|
||||||
|
Mode?: string | undefined;
|
||||||
|
Ports?: Array<{
|
||||||
|
Protocol?: string | undefined;
|
||||||
|
TargetPort?: number | undefined;
|
||||||
|
PublishedPort?: number | undefined;
|
||||||
|
PublishMode?: string | undefined;
|
||||||
|
}> | undefined;
|
||||||
|
}`}
|
||||||
|
</pre>
|
||||||
|
</code>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
|
<FormControl>
|
||||||
|
<CodeEditor
|
||||||
|
language="json"
|
||||||
|
placeholder={`{
|
||||||
|
"Mode": "dnsrr",
|
||||||
|
"Ports": [
|
||||||
|
{
|
||||||
|
"Protocol": "tcp",
|
||||||
|
"TargetPort": 5432,
|
||||||
|
"PublishedPort": 5432,
|
||||||
|
"PublishMode": "host"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`}
|
||||||
|
className="h-[17rem] font-mono"
|
||||||
|
{...field}
|
||||||
|
value={field?.value || ""}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<pre>
|
||||||
|
<FormMessage />
|
||||||
|
</pre>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<DialogFooter className="flex w-full flex-row justify-end md:col-span-2 m-0 sticky bottom-0 right-0 bg-muted border">
|
<DialogFooter className="flex w-full flex-row justify-end md:col-span-2 m-0 sticky bottom-0 right-0 bg-muted border">
|
||||||
<Button
|
<Button
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
|||||||
@@ -59,7 +59,13 @@ const mySchema = z.discriminatedUnion("type", [
|
|||||||
z
|
z
|
||||||
.object({
|
.object({
|
||||||
type: z.literal("volume"),
|
type: z.literal("volume"),
|
||||||
volumeName: z.string().min(1, "Volume name required"),
|
volumeName: z
|
||||||
|
.string()
|
||||||
|
.min(1, "Volume name required")
|
||||||
|
.regex(
|
||||||
|
/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/,
|
||||||
|
"Invalid volume name. Use letters, numbers, '._-' and start with a letter/number.",
|
||||||
|
),
|
||||||
})
|
})
|
||||||
.merge(mountSchema),
|
.merge(mountSchema),
|
||||||
z
|
z
|
||||||
|
|||||||
@@ -41,7 +41,13 @@ const mySchema = z.discriminatedUnion("type", [
|
|||||||
z
|
z
|
||||||
.object({
|
.object({
|
||||||
type: z.literal("volume"),
|
type: z.literal("volume"),
|
||||||
volumeName: z.string().min(1, "Volume name required"),
|
volumeName: z
|
||||||
|
.string()
|
||||||
|
.min(1, "Volume name required")
|
||||||
|
.regex(
|
||||||
|
/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/,
|
||||||
|
"Invalid volume name. Use letters, numbers, '._-' and start with a letter/number.",
|
||||||
|
),
|
||||||
})
|
})
|
||||||
.merge(mountSchema),
|
.merge(mountSchema),
|
||||||
z
|
z
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
import { Clock, Loader2, RefreshCcw, RocketIcon, Settings } from "lucide-react";
|
import {
|
||||||
|
ChevronDown,
|
||||||
|
ChevronUp,
|
||||||
|
Clock,
|
||||||
|
Loader2,
|
||||||
|
RefreshCcw,
|
||||||
|
RocketIcon,
|
||||||
|
Settings,
|
||||||
|
} from "lucide-react";
|
||||||
import React, { useEffect, useMemo, useState } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
@@ -80,6 +88,23 @@ export const ShowDeployments = ({
|
|||||||
} = api.compose.cancelDeployment.useMutation();
|
} = api.compose.cancelDeployment.useMutation();
|
||||||
|
|
||||||
const [url, setUrl] = React.useState("");
|
const [url, setUrl] = React.useState("");
|
||||||
|
const [expandedDescriptions, setExpandedDescriptions] = useState<Set<string>>(
|
||||||
|
new Set(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const MAX_DESCRIPTION_LENGTH = 200;
|
||||||
|
|
||||||
|
const truncateDescription = (description: string): string => {
|
||||||
|
if (description.length <= MAX_DESCRIPTION_LENGTH) {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
const truncated = description.slice(0, MAX_DESCRIPTION_LENGTH);
|
||||||
|
const lastSpace = truncated.lastIndexOf(" ");
|
||||||
|
if (lastSpace > MAX_DESCRIPTION_LENGTH - 20 && lastSpace > 0) {
|
||||||
|
return `${truncated.slice(0, lastSpace)}...`;
|
||||||
|
}
|
||||||
|
return `${truncated}...`;
|
||||||
|
};
|
||||||
|
|
||||||
// Check for stuck deployment (more than 9 minutes) - only for the most recent deployment
|
// Check for stuck deployment (more than 9 minutes) - only for the most recent deployment
|
||||||
const stuckDeployment = useMemo(() => {
|
const stuckDeployment = useMemo(() => {
|
||||||
@@ -217,118 +242,164 @@ export const ShowDeployments = ({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{deployments?.map((deployment, index) => (
|
{deployments?.map((deployment, index) => {
|
||||||
<div
|
const titleText = deployment?.title?.trim() || "";
|
||||||
key={deployment.deploymentId}
|
const needsTruncation = titleText.length > MAX_DESCRIPTION_LENGTH;
|
||||||
className="flex items-center justify-between rounded-lg border p-4 gap-2"
|
const isExpanded = expandedDescriptions.has(
|
||||||
>
|
deployment.deploymentId,
|
||||||
<div className="flex flex-col">
|
);
|
||||||
<span className="flex items-center gap-4 font-medium capitalize text-foreground">
|
|
||||||
{index + 1}. {deployment.status}
|
return (
|
||||||
<StatusTooltip
|
<div
|
||||||
status={deployment?.status}
|
key={deployment.deploymentId}
|
||||||
className="size-2.5"
|
className="flex items-center justify-between rounded-lg border p-4 gap-2"
|
||||||
/>
|
>
|
||||||
</span>
|
<div className="flex flex-col">
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="flex items-center gap-4 font-medium capitalize text-foreground">
|
||||||
{deployment.title}
|
{index + 1}. {deployment.status}
|
||||||
</span>
|
<StatusTooltip
|
||||||
{deployment.description && (
|
status={deployment?.status}
|
||||||
<span className="break-all text-sm text-muted-foreground">
|
className="size-2.5"
|
||||||
{deployment.description}
|
/>
|
||||||
</span>
|
</span>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col items-end gap-2">
|
|
||||||
<div className="text-sm capitalize text-muted-foreground flex items-center gap-2">
|
|
||||||
<DateTooltip date={deployment.createdAt} />
|
|
||||||
{deployment.startedAt && deployment.finishedAt && (
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className="text-[10px] gap-1 flex items-center"
|
|
||||||
>
|
|
||||||
<Clock className="size-3" />
|
|
||||||
{formatDuration(
|
|
||||||
Math.floor(
|
|
||||||
(new Date(deployment.finishedAt).getTime() -
|
|
||||||
new Date(deployment.startedAt).getTime()) /
|
|
||||||
1000,
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-row items-center gap-2">
|
<div className="flex flex-col gap-1">
|
||||||
{deployment.pid && deployment.status === "running" && (
|
<span className="break-words text-sm text-muted-foreground whitespace-pre-wrap">
|
||||||
<DialogAction
|
{isExpanded || !needsTruncation
|
||||||
title="Kill Process"
|
? titleText
|
||||||
description="Are you sure you want to kill the process?"
|
: truncateDescription(titleText)}
|
||||||
type="default"
|
</span>
|
||||||
onClick={async () => {
|
{needsTruncation && (
|
||||||
await killProcess({
|
<button
|
||||||
deploymentId: deployment.deploymentId,
|
type="button"
|
||||||
})
|
onClick={() => {
|
||||||
.then(() => {
|
const next = new Set(expandedDescriptions);
|
||||||
toast.success("Process killed successfully");
|
if (next.has(deployment.deploymentId)) {
|
||||||
})
|
next.delete(deployment.deploymentId);
|
||||||
.catch(() => {
|
} else {
|
||||||
toast.error("Error killing process");
|
next.add(deployment.deploymentId);
|
||||||
});
|
}
|
||||||
}}
|
setExpandedDescriptions(next);
|
||||||
>
|
}}
|
||||||
<Button
|
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors w-fit mt-1 cursor-pointer"
|
||||||
variant="destructive"
|
aria-label={
|
||||||
size="sm"
|
isExpanded
|
||||||
isLoading={isKillingProcess}
|
? "Collapse commit message"
|
||||||
|
: "Expand commit message"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Kill Process
|
{isExpanded ? (
|
||||||
</Button>
|
<>
|
||||||
</DialogAction>
|
<ChevronUp className="size-3" />
|
||||||
)}
|
Show less
|
||||||
<Button
|
</>
|
||||||
onClick={() => {
|
) : (
|
||||||
setActiveLog(deployment);
|
<>
|
||||||
}}
|
<ChevronDown className="size-3" />
|
||||||
>
|
Show more
|
||||||
View
|
</>
|
||||||
</Button>
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{/* Hash (from description) - shown in compact form */}
|
||||||
|
{deployment.description?.trim() && (
|
||||||
|
<span className="text-xs text-muted-foreground font-mono">
|
||||||
|
{deployment.description}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-end gap-2 max-w-[300px] w-full justify-start">
|
||||||
|
<div className="text-sm capitalize text-muted-foreground flex items-center gap-2">
|
||||||
|
<DateTooltip date={deployment.createdAt} />
|
||||||
|
{deployment.startedAt && deployment.finishedAt && (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="text-[10px] gap-1 flex items-center"
|
||||||
|
>
|
||||||
|
<Clock className="size-3" />
|
||||||
|
{formatDuration(
|
||||||
|
Math.floor(
|
||||||
|
(new Date(deployment.finishedAt).getTime() -
|
||||||
|
new Date(deployment.startedAt).getTime()) /
|
||||||
|
1000,
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{deployment?.rollback &&
|
<div className="flex flex-row items-center gap-2">
|
||||||
deployment.status === "done" &&
|
{deployment.pid && deployment.status === "running" && (
|
||||||
type === "application" && (
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Rollback to this deployment"
|
title="Kill Process"
|
||||||
description="Are you sure you want to rollback to this deployment?"
|
description="Are you sure you want to kill the process?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await rollback({
|
await killProcess({
|
||||||
rollbackId: deployment.rollback.rollbackId,
|
deploymentId: deployment.deploymentId,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success(
|
toast.success("Process killed successfully");
|
||||||
"Rollback initiated successfully",
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error initiating rollback");
|
toast.error("Error killing process");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="destructive"
|
||||||
size="sm"
|
size="sm"
|
||||||
isLoading={isRollingBack}
|
isLoading={isKillingProcess}
|
||||||
>
|
>
|
||||||
<RefreshCcw className="size-4 text-primary group-hover:text-red-500" />
|
Kill Process
|
||||||
Rollback
|
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
)}
|
)}
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setActiveLog(deployment);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
View
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{deployment?.rollback &&
|
||||||
|
deployment.status === "done" &&
|
||||||
|
type === "application" && (
|
||||||
|
<DialogAction
|
||||||
|
title="Rollback to this deployment"
|
||||||
|
description="Are you sure you want to rollback to this deployment?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await rollback({
|
||||||
|
rollbackId: deployment.rollback.rollbackId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success(
|
||||||
|
"Rollback initiated successfully",
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error initiating rollback");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
isLoading={isRollingBack}
|
||||||
|
>
|
||||||
|
<RefreshCcw className="size-4 text-primary group-hover:text-red-500" />
|
||||||
|
Rollback
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
))}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<ShowDeployment
|
<ShowDeployment
|
||||||
|
|||||||
@@ -182,7 +182,16 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
|||||||
id={deployment.previewDeploymentId}
|
id={deployment.previewDeploymentId}
|
||||||
type="previewDeployment"
|
type="previewDeployment"
|
||||||
serverId={data?.serverId || ""}
|
serverId={data?.serverId || ""}
|
||||||
/>
|
>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
<RocketIcon className="size-4" />
|
||||||
|
Deployments
|
||||||
|
</Button>
|
||||||
|
</ShowDeploymentsModal>
|
||||||
|
|
||||||
<AddPreviewDomain
|
<AddPreviewDomain
|
||||||
previewDeploymentId={`${deployment.previewDeploymentId}`}
|
previewDeploymentId={`${deployment.previewDeploymentId}`}
|
||||||
|
|||||||
@@ -47,7 +47,13 @@ const formSchema = z
|
|||||||
.object({
|
.object({
|
||||||
name: z.string().min(1, "Name is required"),
|
name: z.string().min(1, "Name is required"),
|
||||||
cronExpression: z.string().min(1, "Cron expression is required"),
|
cronExpression: z.string().min(1, "Cron expression is required"),
|
||||||
volumeName: z.string().min(1, "Volume name is required"),
|
volumeName: z
|
||||||
|
.string()
|
||||||
|
.min(1, "Volume name is required")
|
||||||
|
.regex(
|
||||||
|
/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/,
|
||||||
|
"Invalid volume name. Use letters, numbers, '._-' and start with a letter/number.",
|
||||||
|
),
|
||||||
prefix: z.string(),
|
prefix: z.string(),
|
||||||
keepLatestCount: z.coerce
|
keepLatestCount: z.coerce
|
||||||
.number()
|
.number()
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export const ShowVolumeBackups = ({
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Schedule volume backups to run automatically at specified
|
Schedule volume backups to run automatically at specified
|
||||||
intervals.
|
intervals
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import copy from "copy-to-clipboard";
|
import copy from "copy-to-clipboard";
|
||||||
import { debounce } from "lodash";
|
import _ from "lodash";
|
||||||
import {
|
import {
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
ChevronsUpDown,
|
ChevronsUpDown,
|
||||||
@@ -236,7 +236,7 @@ export const RestoreBackup = ({
|
|||||||
const currentDatabaseType = form.watch("databaseType");
|
const currentDatabaseType = form.watch("databaseType");
|
||||||
const metadata = form.watch("metadata");
|
const metadata = form.watch("metadata");
|
||||||
|
|
||||||
const debouncedSetSearch = debounce((value: string) => {
|
const debouncedSetSearch = _.debounce((value: string) => {
|
||||||
setDebouncedSearchTerm(value);
|
setDebouncedSearchTerm(value);
|
||||||
}, 350);
|
}, 350);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { FancyAnsi } from "fancy-ansi";
|
import { FancyAnsi } from "fancy-ansi";
|
||||||
import { escapeRegExp } from "lodash";
|
import _ from "lodash";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@@ -47,7 +47,7 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const htmlContent = fancyAnsi.toHtml(text);
|
const htmlContent = fancyAnsi.toHtml(text);
|
||||||
const searchRegex = new RegExp(`(${escapeRegExp(term)})`, "gi");
|
const searchRegex = new RegExp(`(${_.escapeRegExp(term)})`, "gi");
|
||||||
|
|
||||||
const modifiedContent = htmlContent.replace(
|
const modifiedContent = htmlContent.replace(
|
||||||
searchRegex,
|
searchRegex,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { useEffect, useMemo, useState } from "react";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||||
|
import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input";
|
||||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
@@ -44,7 +45,6 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input";
|
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -52,12 +52,14 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import { TimeBadge } from "@/components/ui/time-badge";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { HandleProject } from "./handle-project";
|
import { HandleProject } from "./handle-project";
|
||||||
import { ProjectEnvironment } from "./project-environment";
|
import { ProjectEnvironment } from "./project-environment";
|
||||||
|
|
||||||
export const ShowProjects = () => {
|
export const ShowProjects = () => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data, isLoading } = api.project.all.useQuery();
|
const { data, isLoading } = api.project.all.useQuery();
|
||||||
const { data: auth } = api.user.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
const { mutateAsync } = api.project.remove.useMutation();
|
const { mutateAsync } = api.project.remove.useMutation();
|
||||||
@@ -135,6 +137,11 @@ export const ShowProjects = () => {
|
|||||||
<BreadcrumbSidebar
|
<BreadcrumbSidebar
|
||||||
list={[{ name: "Projects", href: "/dashboard/projects" }]}
|
list={[{ name: "Projects", href: "/dashboard/projects" }]}
|
||||||
/>
|
/>
|
||||||
|
{!isCloud && (
|
||||||
|
<div className="absolute top-5 right-5">
|
||||||
|
<TimeBadge />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl ">
|
<Card className="h-full bg-sidebar p-2.5 rounded-xl ">
|
||||||
<div className="rounded-xl bg-background shadow-md ">
|
<div className="rounded-xl bg-background shadow-md ">
|
||||||
@@ -148,7 +155,6 @@ export const ShowProjects = () => {
|
|||||||
Create and manage your projects
|
Create and manage your projects
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
{(auth?.role === "owner" || auth?.canCreateProjects) && (
|
{(auth?.role === "owner" || auth?.canCreateProjects) && (
|
||||||
<div className="">
|
<div className="">
|
||||||
<HandleProject />
|
<HandleProject />
|
||||||
@@ -298,7 +304,13 @@ export const ShowProjects = () => {
|
|||||||
<Link
|
<Link
|
||||||
className="space-x-4 text-xs cursor-pointer justify-between"
|
className="space-x-4 text-xs cursor-pointer justify-between"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
|
href={`${
|
||||||
|
domain.https
|
||||||
|
? "https"
|
||||||
|
: "http"
|
||||||
|
}://${domain.host}${
|
||||||
|
domain.path
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<span className="truncate">
|
<span className="truncate">
|
||||||
{domain.host}
|
{domain.host}
|
||||||
@@ -340,7 +352,13 @@ export const ShowProjects = () => {
|
|||||||
<Link
|
<Link
|
||||||
className="space-x-4 text-xs cursor-pointer justify-between"
|
className="space-x-4 text-xs cursor-pointer justify-between"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
|
href={`${
|
||||||
|
domain.https
|
||||||
|
? "https"
|
||||||
|
: "http"
|
||||||
|
}://${domain.host}${
|
||||||
|
domain.path
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<span className="truncate">
|
<span className="truncate">
|
||||||
{domain.host}
|
{domain.host}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { findEnvironmentById } from "@dokploy/server/index";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
@@ -27,12 +26,10 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { api } from "@/utils/api";
|
import { api, type RouterOutputs } from "@/utils/api";
|
||||||
|
|
||||||
type Environment = Omit<
|
type Project = RouterOutputs["project"]["all"][number];
|
||||||
Awaited<ReturnType<typeof findEnvironmentById>>,
|
type Environment = Project["environments"][number];
|
||||||
"project"
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type Services = {
|
export type Services = {
|
||||||
appName: string;
|
appName: string;
|
||||||
@@ -53,17 +50,16 @@ export type Services = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const extractServices = (data: Environment | undefined) => {
|
export const extractServices = (data: Environment | undefined) => {
|
||||||
const applications: Services[] =
|
const applications: Services[] = (data?.applications?.map((item) => ({
|
||||||
data?.applications.map((item) => ({
|
appName: item.appName,
|
||||||
appName: item.appName,
|
name: item.name,
|
||||||
name: item.name,
|
type: "application",
|
||||||
type: "application",
|
id: item.applicationId,
|
||||||
id: item.applicationId,
|
createdAt: item.createdAt,
|
||||||
createdAt: item.createdAt,
|
status: item.applicationStatus,
|
||||||
status: item.applicationStatus,
|
description: item.description,
|
||||||
description: item.description,
|
serverId: item.serverId,
|
||||||
serverId: item.serverId,
|
})) ?? []) as Services[];
|
||||||
})) || [];
|
|
||||||
|
|
||||||
const mariadb: Services[] =
|
const mariadb: Services[] =
|
||||||
data?.mariadb.map((item) => ({
|
data?.mariadb.map((item) => ({
|
||||||
@@ -125,17 +121,16 @@ export const extractServices = (data: Environment | undefined) => {
|
|||||||
serverId: item.serverId,
|
serverId: item.serverId,
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
const compose: Services[] =
|
const compose: Services[] = (data?.compose?.map((item) => ({
|
||||||
data?.compose.map((item) => ({
|
appName: item.appName,
|
||||||
appName: item.appName,
|
name: item.name,
|
||||||
name: item.name,
|
type: "compose",
|
||||||
type: "compose",
|
id: item.composeId,
|
||||||
id: item.composeId,
|
createdAt: item.createdAt,
|
||||||
createdAt: item.createdAt,
|
status: item.composeStatus,
|
||||||
status: item.composeStatus,
|
description: item.description,
|
||||||
description: item.description,
|
serverId: item.serverId,
|
||||||
serverId: item.serverId,
|
})) ?? []) as Services[];
|
||||||
})) || [];
|
|
||||||
|
|
||||||
applications.push(
|
applications.push(
|
||||||
...mysql,
|
...mysql,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
PieChart,
|
PieChart,
|
||||||
Server,
|
Server,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
|
Star,
|
||||||
Trash2,
|
Trash2,
|
||||||
User,
|
User,
|
||||||
Users,
|
Users,
|
||||||
@@ -82,6 +83,7 @@ import { AddOrganization } from "../dashboard/organization/handle-organization";
|
|||||||
import { DialogAction } from "../shared/dialog-action";
|
import { DialogAction } from "../shared/dialog-action";
|
||||||
import { Logo } from "../shared/logo";
|
import { Logo } from "../shared/logo";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
|
import { TimeBadge } from "../ui/time-badge";
|
||||||
import { UpdateServerButton } from "./update-server";
|
import { UpdateServerButton } from "./update-server";
|
||||||
import { UserNav } from "./user-nav";
|
import { UserNav } from "./user-nav";
|
||||||
|
|
||||||
@@ -497,7 +499,6 @@ function SidebarLogo() {
|
|||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data: user } = api.user.get.useQuery();
|
const { data: user } = api.user.get.useQuery();
|
||||||
const { data: session } = authClient.useSession();
|
const { data: session } = authClient.useSession();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: organizations,
|
data: organizations,
|
||||||
refetch,
|
refetch,
|
||||||
@@ -505,6 +506,8 @@ function SidebarLogo() {
|
|||||||
} = api.organization.all.useQuery();
|
} = api.organization.all.useQuery();
|
||||||
const { mutateAsync: deleteOrganization, isLoading: isRemoving } =
|
const { mutateAsync: deleteOrganization, isLoading: isRemoving } =
|
||||||
api.organization.delete.useMutation();
|
api.organization.delete.useMutation();
|
||||||
|
const { mutateAsync: setDefaultOrganization, isLoading: isSettingDefault } =
|
||||||
|
api.organization.setDefault.useMutation();
|
||||||
const { isMobile } = useSidebar();
|
const { isMobile } = useSidebar();
|
||||||
const { data: activeOrganization } = authClient.useActiveOrganization();
|
const { data: activeOrganization } = authClient.useActiveOrganization();
|
||||||
const _utils = api.useUtils();
|
const _utils = api.useUtils();
|
||||||
@@ -594,66 +597,127 @@ function SidebarLogo() {
|
|||||||
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
||||||
Organizations
|
Organizations
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
{organizations?.map((org) => (
|
{organizations?.map((org) => {
|
||||||
<div className="flex flex-row justify-between" key={org.name}>
|
const isDefault = org.members?.[0]?.isDefault ?? false;
|
||||||
<DropdownMenuItem
|
return (
|
||||||
onClick={async () => {
|
<div
|
||||||
await authClient.organization.setActive({
|
className="flex flex-row justify-between"
|
||||||
organizationId: org.id,
|
key={org.name}
|
||||||
});
|
|
||||||
window.location.reload();
|
|
||||||
}}
|
|
||||||
className="w-full gap-2 p-2"
|
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-4">{org.name}</div>
|
<DropdownMenuItem
|
||||||
<div className="flex size-6 items-center justify-center rounded-sm border">
|
onClick={async () => {
|
||||||
<Logo
|
await authClient.organization.setActive({
|
||||||
className={cn(
|
organizationId: org.id,
|
||||||
"transition-all",
|
});
|
||||||
state === "collapsed" ? "size-6" : "size-10",
|
window.location.reload();
|
||||||
)}
|
}}
|
||||||
logoUrl={org.logo ?? undefined}
|
className="w-full gap-2 p-2"
|
||||||
/>
|
>
|
||||||
</div>
|
<div className="flex flex-col gap-1">
|
||||||
</DropdownMenuItem>
|
<div className="flex items-center gap-2">
|
||||||
{org.ownerId === session?.user?.id && (
|
{org.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex size-6 items-center justify-center rounded-sm border">
|
||||||
|
<Logo
|
||||||
|
className={cn(
|
||||||
|
"transition-all",
|
||||||
|
state === "collapsed" ? "size-6" : "size-10",
|
||||||
|
)}
|
||||||
|
logoUrl={org.logo ?? undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<AddOrganization organizationId={org.id} />
|
<Button
|
||||||
<DialogAction
|
variant="ghost"
|
||||||
title="Delete Organization"
|
size="icon"
|
||||||
description="Are you sure you want to delete this organization?"
|
className={cn(
|
||||||
type="destructive"
|
"group",
|
||||||
onClick={async () => {
|
isDefault
|
||||||
await deleteOrganization({
|
? "hover:bg-yellow-500/10"
|
||||||
|
: "hover:bg-blue-500/10",
|
||||||
|
)}
|
||||||
|
isLoading={isSettingDefault && !isDefault}
|
||||||
|
disabled={isDefault}
|
||||||
|
onClick={async (e) => {
|
||||||
|
if (isDefault) return;
|
||||||
|
e.stopPropagation();
|
||||||
|
await setDefaultOrganization({
|
||||||
organizationId: org.id,
|
organizationId: org.id,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
refetch();
|
refetch();
|
||||||
toast.success(
|
toast.success("Default organization updated");
|
||||||
"Organization deleted successfully",
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
toast.error(
|
toast.error(
|
||||||
error?.message ||
|
error?.message ||
|
||||||
"Error deleting organization",
|
"Error setting default organization",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
title={
|
||||||
|
isDefault
|
||||||
|
? "Default organization"
|
||||||
|
: "Set as default"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Button
|
{isDefault ? (
|
||||||
variant="ghost"
|
<Star
|
||||||
size="icon"
|
fill="#eab308"
|
||||||
className="group hover:bg-red-500/10"
|
stroke="#eab308"
|
||||||
isLoading={isRemoving}
|
className="size-4 text-yellow-500"
|
||||||
>
|
/>
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
) : (
|
||||||
</Button>
|
<Star
|
||||||
</DialogAction>
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
className="size-4 text-gray-400 group-hover:text-blue-500 transition-colors"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
{org.ownerId === session?.user?.id && (
|
||||||
|
<>
|
||||||
|
<AddOrganization organizationId={org.id} />
|
||||||
|
<DialogAction
|
||||||
|
title="Delete Organization"
|
||||||
|
description="Are you sure you want to delete this organization?"
|
||||||
|
type="destructive"
|
||||||
|
onClick={async () => {
|
||||||
|
await deleteOrganization({
|
||||||
|
organizationId: org.id,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
refetch();
|
||||||
|
toast.success(
|
||||||
|
"Organization deleted successfully",
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
toast.error(
|
||||||
|
error?.message ||
|
||||||
|
"Error deleting organization",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="group hover:bg-red-500/10"
|
||||||
|
isLoading={isRemoving}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
);
|
||||||
))}
|
})}
|
||||||
{(user?.role === "owner" || isCloud) && (
|
{(user?.role === "owner" || isCloud) && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
@@ -1062,6 +1126,7 @@ export default function Page({ children }: Props) {
|
|||||||
</BreadcrumbList>
|
</BreadcrumbList>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
</div>
|
</div>
|
||||||
|
{!isCloud && <TimeBadge />}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -67,9 +67,10 @@ export const Dropzone = React.forwardRef<HTMLDivElement, DropzoneProps>(
|
|||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
type="file"
|
type="file"
|
||||||
className={cn("hidden", className)}
|
className={cn("hidden", className)}
|
||||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||||
onChange(e.target.files)
|
onChange(e.target.files);
|
||||||
}
|
e.target.value = "";
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
58
apps/dokploy/components/ui/time-badge.tsx
Normal file
58
apps/dokploy/components/ui/time-badge.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
|
export function TimeBadge() {
|
||||||
|
const { data: serverTime } = api.server.getServerTime.useQuery(undefined);
|
||||||
|
const [time, setTime] = useState<Date | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (serverTime?.time) {
|
||||||
|
setTime(new Date(serverTime.time));
|
||||||
|
}
|
||||||
|
}, [serverTime]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
setTime((prevTime) => {
|
||||||
|
if (!prevTime) return null;
|
||||||
|
const newTime = new Date(prevTime.getTime() + 1000);
|
||||||
|
return newTime;
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(timer);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!time || !serverTime?.timezone) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUtcOffset = (timeZone: string) => {
|
||||||
|
const date = new Date();
|
||||||
|
const utcDate = new Date(date.toLocaleString("en-US", { timeZone: "UTC" }));
|
||||||
|
const tzDate = new Date(date.toLocaleString("en-US", { timeZone }));
|
||||||
|
const offset = (tzDate.getTime() - utcDate.getTime()) / (1000 * 60 * 60);
|
||||||
|
const sign = offset >= 0 ? "+" : "-";
|
||||||
|
const hours = Math.floor(Math.abs(offset));
|
||||||
|
const minutes = (Math.abs(offset) * 60) % 60;
|
||||||
|
return `UTC${sign}${hours.toString().padStart(2, "0")}:${minutes
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0")}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="inline-flex items-center gap-2 rounded-md border px-2 py-1 text-xs sm:text-sm whitespace-nowrap max-w-full overflow-hidden">
|
||||||
|
<span className="hidden sm:inline">Server Time:</span>
|
||||||
|
<span className="font-medium tabular-nums">
|
||||||
|
{time.toLocaleTimeString()}
|
||||||
|
</span>
|
||||||
|
<span className="hidden sm:inline text-muted-foreground">
|
||||||
|
({serverTime.timezone} | {getUtcOffset(serverTime.timezone)})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
1
apps/dokploy/drizzle/0119_bouncy_morbius.sql
Normal file
1
apps/dokploy/drizzle/0119_bouncy_morbius.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "member" ADD COLUMN "is_default" boolean DEFAULT false NOT NULL;
|
||||||
39
apps/dokploy/drizzle/0120_lame_captain_midlands.sql
Normal file
39
apps/dokploy/drizzle/0120_lame_captain_midlands.sql
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
ALTER TABLE "user_temp" RENAME TO "user";--> statement-breakpoint
|
||||||
|
ALTER TABLE "user" DROP CONSTRAINT "user_temp_email_unique";--> statement-breakpoint
|
||||||
|
ALTER TABLE "account" DROP CONSTRAINT "account_user_id_user_temp_id_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "apikey" DROP CONSTRAINT "apikey_user_id_user_temp_id_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "invitation" DROP CONSTRAINT "invitation_inviter_id_user_temp_id_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "member" DROP CONSTRAINT "member_user_id_user_temp_id_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "organization" DROP CONSTRAINT "organization_owner_id_user_temp_id_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "two_factor" DROP CONSTRAINT "two_factor_user_id_user_temp_id_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "backup" DROP CONSTRAINT "backup_userId_user_temp_id_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "git_provider" DROP CONSTRAINT "git_provider_userId_user_temp_id_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "schedule" DROP CONSTRAINT "schedule_userId_user_temp_id_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "session_temp" DROP CONSTRAINT "session_temp_user_id_user_temp_id_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "application" ADD COLUMN "endpointSpecSwarm" json;--> statement-breakpoint
|
||||||
|
ALTER TABLE "mariadb" ADD COLUMN "endpointSpecSwarm" json;--> statement-breakpoint
|
||||||
|
ALTER TABLE "mongo" ADD COLUMN "endpointSpecSwarm" json;--> statement-breakpoint
|
||||||
|
ALTER TABLE "mysql" ADD COLUMN "endpointSpecSwarm" json;--> statement-breakpoint
|
||||||
|
ALTER TABLE "postgres" ADD COLUMN "endpointSpecSwarm" json;--> statement-breakpoint
|
||||||
|
ALTER TABLE "redis" ADD COLUMN "endpointSpecSwarm" json;--> statement-breakpoint
|
||||||
|
ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "apikey" ADD CONSTRAINT "apikey_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "invitation" ADD CONSTRAINT "invitation_inviter_id_user_id_fk" FOREIGN KEY ("inviter_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "member" ADD CONSTRAINT "member_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "organization" ADD CONSTRAINT "organization_owner_id_user_id_fk" FOREIGN KEY ("owner_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "two_factor" ADD CONSTRAINT "two_factor_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "backup" ADD CONSTRAINT "backup_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "git_provider" ADD CONSTRAINT "git_provider_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "schedule" ADD CONSTRAINT "schedule_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "session_temp" ADD CONSTRAINT "session_temp_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "user" ADD CONSTRAINT "user_email_unique" UNIQUE("email");
|
||||||
6686
apps/dokploy/drizzle/meta/0119_snapshot.json
Normal file
6686
apps/dokploy/drizzle/meta/0119_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
6722
apps/dokploy/drizzle/meta/0120_snapshot.json
Normal file
6722
apps/dokploy/drizzle/meta/0120_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -834,6 +834,20 @@
|
|||||||
"when": 1761415824484,
|
"when": 1761415824484,
|
||||||
"tag": "0118_loose_anita_blake",
|
"tag": "0118_loose_anita_blake",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 119,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1762142756443,
|
||||||
|
"tag": "0119_bouncy_morbius",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 120,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1762632540024,
|
||||||
|
"tag": "0120_lame_captain_midlands",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.25.6",
|
"version": "v0.25.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -120,7 +120,7 @@
|
|||||||
"next": "^15.3.2",
|
"next": "^15.3.2",
|
||||||
"next-i18next": "^15.4.2",
|
"next-i18next": "^15.4.2",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"node-os-utils": "1.3.7",
|
"node-os-utils": "2.0.1",
|
||||||
"node-pty": "1.0.0",
|
"node-pty": "1.0.0",
|
||||||
"node-schedule": "2.1.1",
|
"node-schedule": "2.1.1",
|
||||||
"nodemailer": "6.9.14",
|
"nodemailer": "6.9.14",
|
||||||
@@ -163,7 +163,6 @@
|
|||||||
"@types/lodash": "4.17.4",
|
"@types/lodash": "4.17.4",
|
||||||
"@types/micromatch": "4.0.9",
|
"@types/micromatch": "4.0.9",
|
||||||
"@types/node": "^18.19.104",
|
"@types/node": "^18.19.104",
|
||||||
"@types/node-os-utils": "1.3.4",
|
|
||||||
"@types/node-schedule": "2.1.6",
|
"@types/node-schedule": "2.1.6",
|
||||||
"@types/nodemailer": "^6.4.17",
|
"@types/nodemailer": "^6.4.17",
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
|
|||||||
@@ -12,6 +12,17 @@ import type { DeploymentJob } from "@/server/queues/queue-types";
|
|||||||
import { myQueue } from "@/server/queues/queueSetup";
|
import { myQueue } from "@/server/queues/queueSetup";
|
||||||
import { deploy } from "@/server/utils/deploy";
|
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(
|
export default async function handler(
|
||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
res: NextApiResponse,
|
res: NextApiResponse,
|
||||||
@@ -46,21 +57,60 @@ export default async function handler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const deploymentTitle = extractCommitMessage(req.headers, req.body);
|
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;
|
const sourceType = application.sourceType;
|
||||||
|
|
||||||
if (sourceType === "docker") {
|
if (sourceType === "docker") {
|
||||||
|
const applicationImageName = extractImageName(application.dockerImage);
|
||||||
const applicationDockerTag = extractImageTag(application.dockerImage);
|
const applicationDockerTag = extractImageTag(application.dockerImage);
|
||||||
|
|
||||||
|
const webhookImageName = extractImageNameFromRequest(
|
||||||
|
req.headers,
|
||||||
|
req.body,
|
||||||
|
);
|
||||||
const webhookDockerTag = extractImageTagFromRequest(
|
const webhookDockerTag = extractImageTagFromRequest(
|
||||||
req.headers,
|
req.headers,
|
||||||
req.body,
|
req.body,
|
||||||
);
|
);
|
||||||
if (
|
|
||||||
applicationDockerTag &&
|
if (!applicationImageName) {
|
||||||
webhookDockerTag &&
|
res.status(301).json({
|
||||||
webhookDockerTag !== applicationDockerTag
|
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({
|
res.status(301).json({
|
||||||
message: `Application Image Tag (${applicationDockerTag}) doesn't match request event payload Image Tag (${webhookDockerTag}).`,
|
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 = {
|
const jobData: DeploymentJob = {
|
||||||
applicationId: application.applicationId as string,
|
applicationId: application.applicationId as string,
|
||||||
titleLog: deploymentTitle,
|
titleLog: deploymentTitle,
|
||||||
descriptionLog: `Hash: ${deploymentHash}`,
|
...(deploymentHash && { descriptionLog: `Hash: ${deploymentHash}` }),
|
||||||
type: "deploy",
|
type: "deploy",
|
||||||
applicationType: "application",
|
applicationType: "application",
|
||||||
server: !!application.serverId,
|
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
|
* Return the last part of the image name, which is the tag
|
||||||
* Example: "my-image" => null
|
* Example: "my-image" => null
|
||||||
@@ -230,7 +313,7 @@ export default async function handler(
|
|||||||
* Example: "myregistryhost:5000/fedora/httpd:version1.0" => "version1.0"
|
* Example: "myregistryhost:5000/fedora/httpd:version1.0" => "version1.0"
|
||||||
* @link https://docs.docker.com/reference/cli/docker/image/tag/
|
* @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") {
|
if (!dockerImage || typeof dockerImage !== "string") {
|
||||||
return null;
|
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.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 = (
|
export const extractImageTagFromRequest = (
|
||||||
headers: any,
|
headers: any,
|
||||||
body: any,
|
body: any,
|
||||||
): string | null => {
|
): 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 (headers["user-agent"]?.includes("Go-http-client")) {
|
||||||
if (body.push_data && body.repository) {
|
if (body.push_data && body.repository) {
|
||||||
return body.push_data.tag;
|
return body.push_data.tag;
|
||||||
@@ -255,6 +404,18 @@ export const extractImageTagFromRequest = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const extractCommitMessage = (headers: any, body: any) => {
|
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
|
// GitHub
|
||||||
if (headers["x-github-event"]) {
|
if (headers["x-github-event"]) {
|
||||||
return body.head_commit ? body.head_commit.message : "NEW COMMIT";
|
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 (headers["user-agent"]?.includes("Go-http-client")) {
|
||||||
if (body.push_data && body.repository) {
|
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}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { asc, eq } from "drizzle-orm";
|
|||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import Stripe from "stripe";
|
import Stripe from "stripe";
|
||||||
import { db } from "@/server/db";
|
import { db } from "@/server/db";
|
||||||
import { organization, server, users_temp } from "@/server/db/schema";
|
import { organization, server, user } from "@/server/db/schema";
|
||||||
|
|
||||||
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET!;
|
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET!;
|
||||||
|
|
||||||
@@ -64,13 +64,13 @@ export default async function handler(
|
|||||||
session.subscription as string,
|
session.subscription as string,
|
||||||
);
|
);
|
||||||
await db
|
await db
|
||||||
.update(users_temp)
|
.update(user)
|
||||||
.set({
|
.set({
|
||||||
stripeCustomerId: session.customer as string,
|
stripeCustomerId: session.customer as string,
|
||||||
stripeSubscriptionId: session.subscription as string,
|
stripeSubscriptionId: session.subscription as string,
|
||||||
serversQuantity: subscription?.items?.data?.[0]?.quantity ?? 0,
|
serversQuantity: subscription?.items?.data?.[0]?.quantity ?? 0,
|
||||||
})
|
})
|
||||||
.where(eq(users_temp.id, adminId))
|
.where(eq(user.id, adminId))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
const admin = await findUserById(adminId);
|
const admin = await findUserById(adminId);
|
||||||
@@ -85,14 +85,12 @@ export default async function handler(
|
|||||||
const newSubscription = event.data.object as Stripe.Subscription;
|
const newSubscription = event.data.object as Stripe.Subscription;
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.update(users_temp)
|
.update(user)
|
||||||
.set({
|
.set({
|
||||||
stripeSubscriptionId: newSubscription.id,
|
stripeSubscriptionId: newSubscription.id,
|
||||||
stripeCustomerId: newSubscription.customer as string,
|
stripeCustomerId: newSubscription.customer as string,
|
||||||
})
|
})
|
||||||
.where(
|
.where(eq(user.stripeCustomerId, newSubscription.customer as string))
|
||||||
eq(users_temp.stripeCustomerId, newSubscription.customer as string),
|
|
||||||
)
|
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -102,14 +100,12 @@ export default async function handler(
|
|||||||
const newSubscription = event.data.object as Stripe.Subscription;
|
const newSubscription = event.data.object as Stripe.Subscription;
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.update(users_temp)
|
.update(user)
|
||||||
.set({
|
.set({
|
||||||
stripeSubscriptionId: null,
|
stripeSubscriptionId: null,
|
||||||
serversQuantity: 0,
|
serversQuantity: 0,
|
||||||
})
|
})
|
||||||
.where(
|
.where(eq(user.stripeCustomerId, newSubscription.customer as string));
|
||||||
eq(users_temp.stripeCustomerId, newSubscription.customer as string),
|
|
||||||
);
|
|
||||||
|
|
||||||
const admin = await findUserByStripeCustomerId(
|
const admin = await findUserByStripeCustomerId(
|
||||||
newSubscription.customer as string,
|
newSubscription.customer as string,
|
||||||
@@ -135,24 +131,20 @@ export default async function handler(
|
|||||||
|
|
||||||
if (newSubscription.status === "active") {
|
if (newSubscription.status === "active") {
|
||||||
await db
|
await db
|
||||||
.update(users_temp)
|
.update(user)
|
||||||
.set({
|
.set({
|
||||||
serversQuantity: newSubscription?.items?.data?.[0]?.quantity ?? 0,
|
serversQuantity: newSubscription?.items?.data?.[0]?.quantity ?? 0,
|
||||||
})
|
})
|
||||||
.where(
|
.where(eq(user.stripeCustomerId, newSubscription.customer as string));
|
||||||
eq(users_temp.stripeCustomerId, newSubscription.customer as string),
|
|
||||||
);
|
|
||||||
|
|
||||||
const newServersQuantity = admin.serversQuantity;
|
const newServersQuantity = admin.serversQuantity;
|
||||||
await updateServersBasedOnQuantity(admin.id, newServersQuantity);
|
await updateServersBasedOnQuantity(admin.id, newServersQuantity);
|
||||||
} else {
|
} else {
|
||||||
await disableServers(admin.id);
|
await disableServers(admin.id);
|
||||||
await db
|
await db
|
||||||
.update(users_temp)
|
.update(user)
|
||||||
.set({ serversQuantity: 0 })
|
.set({ serversQuantity: 0 })
|
||||||
.where(
|
.where(eq(user.stripeCustomerId, newSubscription.customer as string));
|
||||||
eq(users_temp.stripeCustomerId, newSubscription.customer as string),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -172,11 +164,11 @@ export default async function handler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.update(users_temp)
|
.update(user)
|
||||||
.set({
|
.set({
|
||||||
serversQuantity: suscription?.items?.data?.[0]?.quantity ?? 0,
|
serversQuantity: suscription?.items?.data?.[0]?.quantity ?? 0,
|
||||||
})
|
})
|
||||||
.where(eq(users_temp.stripeCustomerId, suscription.customer as string));
|
.where(eq(user.stripeCustomerId, suscription.customer as string));
|
||||||
|
|
||||||
const admin = await findUserByStripeCustomerId(
|
const admin = await findUserByStripeCustomerId(
|
||||||
suscription.customer as string,
|
suscription.customer as string,
|
||||||
@@ -205,13 +197,11 @@ export default async function handler(
|
|||||||
return res.status(400).send("Webhook Error: Admin not found");
|
return res.status(400).send("Webhook Error: Admin not found");
|
||||||
}
|
}
|
||||||
await db
|
await db
|
||||||
.update(users_temp)
|
.update(user)
|
||||||
.set({
|
.set({
|
||||||
serversQuantity: 0,
|
serversQuantity: 0,
|
||||||
})
|
})
|
||||||
.where(
|
.where(eq(user.stripeCustomerId, newInvoice.customer as string));
|
||||||
eq(users_temp.stripeCustomerId, newInvoice.customer as string),
|
|
||||||
);
|
|
||||||
|
|
||||||
await disableServers(admin.id);
|
await disableServers(admin.id);
|
||||||
}
|
}
|
||||||
@@ -229,13 +219,13 @@ export default async function handler(
|
|||||||
|
|
||||||
await disableServers(admin.id);
|
await disableServers(admin.id);
|
||||||
await db
|
await db
|
||||||
.update(users_temp)
|
.update(user)
|
||||||
.set({
|
.set({
|
||||||
stripeCustomerId: null,
|
stripeCustomerId: null,
|
||||||
stripeSubscriptionId: null,
|
stripeSubscriptionId: null,
|
||||||
serversQuantity: 0,
|
serversQuantity: 0,
|
||||||
})
|
})
|
||||||
.where(eq(users_temp.stripeCustomerId, customer.id));
|
.where(eq(user.stripeCustomerId, customer.id));
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -262,10 +252,10 @@ const disableServers = async (userId: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const findUserByStripeCustomerId = async (stripeCustomerId: string) => {
|
const findUserByStripeCustomerId = async (stripeCustomerId: string) => {
|
||||||
const user = db.query.users_temp.findFirst({
|
const userResult = await db.query.user.findFirst({
|
||||||
where: eq(users_temp.stripeCustomerId, stripeCustomerId),
|
where: eq(user.stripeCustomerId, stripeCustomerId),
|
||||||
});
|
});
|
||||||
return user;
|
return userResult;
|
||||||
};
|
};
|
||||||
|
|
||||||
const activateServer = async (serverId: string) => {
|
const activateServer = async (serverId: string) => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { findProjectById } from "@dokploy/server";
|
import type { findEnvironmentById } from "@dokploy/server";
|
||||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import {
|
import {
|
||||||
@@ -102,6 +102,7 @@ import { api } from "@/utils/api";
|
|||||||
export type Services = {
|
export type Services = {
|
||||||
appName: string;
|
appName: string;
|
||||||
serverId?: string | null;
|
serverId?: string | null;
|
||||||
|
serverName?: string | null;
|
||||||
name: string;
|
name: string;
|
||||||
type:
|
type:
|
||||||
| "mariadb"
|
| "mariadb"
|
||||||
@@ -115,10 +116,10 @@ export type Services = {
|
|||||||
id: string;
|
id: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
status?: "idle" | "running" | "done" | "error";
|
status?: "idle" | "running" | "done" | "error";
|
||||||
|
lastDeployDate?: Date | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Project = Awaited<ReturnType<typeof findProjectById>>;
|
type Environment = Awaited<ReturnType<typeof findEnvironmentById>>;
|
||||||
type Environment = Project["environments"][0];
|
|
||||||
|
|
||||||
export const extractServicesFromEnvironment = (
|
export const extractServicesFromEnvironment = (
|
||||||
environment: Environment | undefined,
|
environment: Environment | undefined,
|
||||||
@@ -128,16 +129,35 @@ export const extractServicesFromEnvironment = (
|
|||||||
const allServices: Services[] = [];
|
const allServices: Services[] = [];
|
||||||
|
|
||||||
const applications: Services[] =
|
const applications: Services[] =
|
||||||
environment.applications?.map((item) => ({
|
environment.applications?.map((item) => {
|
||||||
appName: item.appName,
|
// Get the most recent deployment date
|
||||||
name: item.name,
|
let lastDeployDate: Date | null = null;
|
||||||
type: "application",
|
const deployments = (item as any).deployments;
|
||||||
id: item.applicationId,
|
if (deployments && deployments.length > 0) {
|
||||||
createdAt: item.createdAt,
|
for (const deployment of deployments) {
|
||||||
status: item.applicationStatus,
|
const deployDate = new Date(
|
||||||
description: item.description,
|
deployment.finishedAt ||
|
||||||
serverId: item.serverId,
|
deployment.startedAt ||
|
||||||
})) || [];
|
deployment.createdAt,
|
||||||
|
);
|
||||||
|
if (!lastDeployDate || deployDate > lastDeployDate) {
|
||||||
|
lastDeployDate = deployDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
appName: item.appName,
|
||||||
|
name: item.name,
|
||||||
|
type: "application",
|
||||||
|
id: item.applicationId,
|
||||||
|
createdAt: item.createdAt,
|
||||||
|
status: item.applicationStatus,
|
||||||
|
description: item.description,
|
||||||
|
serverId: item.serverId,
|
||||||
|
serverName: item?.server?.name || null,
|
||||||
|
lastDeployDate,
|
||||||
|
};
|
||||||
|
}) || [];
|
||||||
|
|
||||||
const mariadb: Services[] =
|
const mariadb: Services[] =
|
||||||
environment.mariadb?.map((item) => ({
|
environment.mariadb?.map((item) => ({
|
||||||
@@ -149,6 +169,7 @@ export const extractServicesFromEnvironment = (
|
|||||||
status: item.applicationStatus,
|
status: item.applicationStatus,
|
||||||
description: item.description,
|
description: item.description,
|
||||||
serverId: item.serverId,
|
serverId: item.serverId,
|
||||||
|
serverName: item?.server?.name || null,
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
const postgres: Services[] =
|
const postgres: Services[] =
|
||||||
@@ -161,6 +182,7 @@ export const extractServicesFromEnvironment = (
|
|||||||
status: item.applicationStatus,
|
status: item.applicationStatus,
|
||||||
description: item.description,
|
description: item.description,
|
||||||
serverId: item.serverId,
|
serverId: item.serverId,
|
||||||
|
serverName: item?.server?.name || null,
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
const mongo: Services[] =
|
const mongo: Services[] =
|
||||||
@@ -173,6 +195,7 @@ export const extractServicesFromEnvironment = (
|
|||||||
status: item.applicationStatus,
|
status: item.applicationStatus,
|
||||||
description: item.description,
|
description: item.description,
|
||||||
serverId: item.serverId,
|
serverId: item.serverId,
|
||||||
|
serverName: item?.server?.name || null,
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
const redis: Services[] =
|
const redis: Services[] =
|
||||||
@@ -185,6 +208,7 @@ export const extractServicesFromEnvironment = (
|
|||||||
status: item.applicationStatus,
|
status: item.applicationStatus,
|
||||||
description: item.description,
|
description: item.description,
|
||||||
serverId: item.serverId,
|
serverId: item.serverId,
|
||||||
|
serverName: item?.server?.name || null,
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
const mysql: Services[] =
|
const mysql: Services[] =
|
||||||
@@ -197,19 +221,39 @@ export const extractServicesFromEnvironment = (
|
|||||||
status: item.applicationStatus,
|
status: item.applicationStatus,
|
||||||
description: item.description,
|
description: item.description,
|
||||||
serverId: item.serverId,
|
serverId: item.serverId,
|
||||||
|
serverName: item?.server?.name || null,
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
const compose: Services[] =
|
const compose: Services[] =
|
||||||
environment.compose?.map((item) => ({
|
environment.compose?.map((item) => {
|
||||||
appName: item.appName,
|
// Get the most recent deployment date
|
||||||
name: item.name,
|
let lastDeployDate: Date | null = null;
|
||||||
type: "compose",
|
const deployments = (item as any).deployments;
|
||||||
id: item.composeId,
|
if (deployments && deployments.length > 0) {
|
||||||
createdAt: item.createdAt,
|
for (const deployment of deployments) {
|
||||||
status: item.composeStatus,
|
const deployDate = new Date(
|
||||||
description: item.description,
|
deployment.finishedAt ||
|
||||||
serverId: item.serverId,
|
deployment.startedAt ||
|
||||||
})) || [];
|
deployment.createdAt,
|
||||||
|
);
|
||||||
|
if (!lastDeployDate || deployDate > lastDeployDate) {
|
||||||
|
lastDeployDate = deployDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
appName: item.appName,
|
||||||
|
name: item.name,
|
||||||
|
type: "compose",
|
||||||
|
id: item.composeId,
|
||||||
|
createdAt: item.createdAt,
|
||||||
|
status: item.composeStatus,
|
||||||
|
description: item.description,
|
||||||
|
serverId: item.serverId,
|
||||||
|
serverName: item?.server?.name || null,
|
||||||
|
lastDeployDate,
|
||||||
|
};
|
||||||
|
}) || [];
|
||||||
|
|
||||||
allServices.push(
|
allServices.push(
|
||||||
...applications,
|
...applications,
|
||||||
@@ -237,9 +281,9 @@ const EnvironmentPage = (
|
|||||||
const { data: auth } = api.user.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
const [sortBy, setSortBy] = useState<string>(() => {
|
const [sortBy, setSortBy] = useState<string>(() => {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
return localStorage.getItem("servicesSort") || "createdAt-desc";
|
return localStorage.getItem("servicesSort") || "lastDeploy-desc";
|
||||||
}
|
}
|
||||||
return "createdAt-desc";
|
return "lastDeploy-desc";
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -261,10 +305,45 @@ const EnvironmentPage = (
|
|||||||
comparison =
|
comparison =
|
||||||
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
||||||
break;
|
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:
|
default:
|
||||||
comparison = 0;
|
comparison = 0;
|
||||||
}
|
}
|
||||||
return direction === "asc" ? comparison : -comparison;
|
// For other fields, apply direction normally
|
||||||
|
if (field !== "lastDeploy") {
|
||||||
|
return direction === "asc" ? comparison : -comparison;
|
||||||
|
}
|
||||||
|
return comparison;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -320,6 +399,7 @@ const EnvironmentPage = (
|
|||||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||||
const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false);
|
const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false);
|
||||||
const [deleteVolumes, setDeleteVolumes] = useState(false);
|
const [deleteVolumes, setDeleteVolumes] = useState(false);
|
||||||
|
const [selectedServerId, setSelectedServerId] = useState<string>("all");
|
||||||
|
|
||||||
const handleSelectAll = () => {
|
const handleSelectAll = () => {
|
||||||
if (selectedServices.length === filteredServices.length) {
|
if (selectedServices.length === filteredServices.length) {
|
||||||
@@ -709,6 +789,27 @@ const EnvironmentPage = (
|
|||||||
setIsBulkActionLoading(false);
|
setIsBulkActionLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Get unique servers from services
|
||||||
|
const availableServers = useMemo(() => {
|
||||||
|
if (!applications) return [];
|
||||||
|
const servers = new Map<string, { serverId: string; serverName: string }>();
|
||||||
|
applications.forEach((service) => {
|
||||||
|
if (service.serverId && service.serverName) {
|
||||||
|
servers.set(service.serverId, {
|
||||||
|
serverId: service.serverId,
|
||||||
|
serverName: service.serverName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Array.from(servers.values());
|
||||||
|
}, [applications]);
|
||||||
|
|
||||||
|
// Check if there are services without a server (Dokploy server)
|
||||||
|
const hasServicesWithoutServer = useMemo(() => {
|
||||||
|
if (!applications) return false;
|
||||||
|
return applications.some((service) => !service.serverId);
|
||||||
|
}, [applications]);
|
||||||
|
|
||||||
const filteredServices = useMemo(() => {
|
const filteredServices = useMemo(() => {
|
||||||
if (!applications) return [];
|
if (!applications) return [];
|
||||||
const filtered = applications.filter(
|
const filtered = applications.filter(
|
||||||
@@ -717,10 +818,14 @@ const EnvironmentPage = (
|
|||||||
service.description
|
service.description
|
||||||
?.toLowerCase()
|
?.toLowerCase()
|
||||||
.includes(searchQuery.toLowerCase())) &&
|
.includes(searchQuery.toLowerCase())) &&
|
||||||
(selectedTypes.length === 0 || selectedTypes.includes(service.type)),
|
(selectedTypes.length === 0 || selectedTypes.includes(service.type)) &&
|
||||||
|
(selectedServerId === "" ||
|
||||||
|
selectedServerId === "all" ||
|
||||||
|
(selectedServerId === "dokploy-server" && !service.serverId) ||
|
||||||
|
service.serverId === selectedServerId),
|
||||||
);
|
);
|
||||||
return sortServices(filtered);
|
return sortServices(filtered);
|
||||||
}, [applications, searchQuery, selectedTypes, sortBy]);
|
}, [applications, searchQuery, selectedTypes, selectedServerId, sortBy]);
|
||||||
|
|
||||||
const selectedServicesWithRunningStatus = useMemo(() => {
|
const selectedServicesWithRunningStatus = useMemo(() => {
|
||||||
return filteredServices.filter(
|
return filteredServices.filter(
|
||||||
@@ -1217,6 +1322,9 @@ const EnvironmentPage = (
|
|||||||
<SelectValue placeholder="Sort by..." />
|
<SelectValue placeholder="Sort by..." />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
<SelectItem value="lastDeploy-desc">
|
||||||
|
Recently deployed
|
||||||
|
</SelectItem>
|
||||||
<SelectItem value="createdAt-desc">
|
<SelectItem value="createdAt-desc">
|
||||||
Newest first
|
Newest first
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
@@ -1291,6 +1399,39 @@ const EnvironmentPage = (
|
|||||||
</Command>
|
</Command>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
{(availableServers.length > 0 ||
|
||||||
|
hasServicesWithoutServer) && (
|
||||||
|
<Select
|
||||||
|
value={selectedServerId || "all"}
|
||||||
|
onValueChange={setSelectedServerId}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="lg:w-[200px]">
|
||||||
|
<SelectValue placeholder="Filter by server..." />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">All servers</SelectItem>
|
||||||
|
{hasServicesWithoutServer && (
|
||||||
|
<SelectItem value="dokploy-server">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<ServerIcon className="size-4" />
|
||||||
|
<span>Dokploy server</span>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
{availableServers.map((server) => (
|
||||||
|
<SelectItem
|
||||||
|
key={server.serverId}
|
||||||
|
value={server.serverId}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<ServerIcon className="size-4" />
|
||||||
|
<span>{server.serverName}</span>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1396,7 +1537,15 @@ const EnvironmentPage = (
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardFooter className="mt-auto">
|
<CardFooter className="mt-auto">
|
||||||
<div className="space-y-1 text-sm">
|
<div className="space-y-1 text-sm w-full">
|
||||||
|
{service.serverName && (
|
||||||
|
<div className="flex items-center gap-1.5 text-xs text-muted-foreground mb-1">
|
||||||
|
<ServerIcon className="size-3" />
|
||||||
|
<span className="truncate">
|
||||||
|
{service.serverName}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<DateTooltip date={service.createdAt}>
|
<DateTooltip date={service.createdAt}>
|
||||||
Created
|
Created
|
||||||
</DateTooltip>
|
</DateTooltip>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { findAdmin } from "@dokploy/server";
|
import { findAdmin } from "@dokploy/server";
|
||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
import { users_temp } from "@dokploy/server/db/schema";
|
import { user } from "@dokploy/server/db/schema";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
@@ -8,11 +8,11 @@ import { eq } from "drizzle-orm";
|
|||||||
const result = await findAdmin();
|
const result = await findAdmin();
|
||||||
|
|
||||||
const update = await db
|
const update = await db
|
||||||
.update(users_temp)
|
.update(user)
|
||||||
.set({
|
.set({
|
||||||
twoFactorEnabled: false,
|
twoFactorEnabled: false,
|
||||||
})
|
})
|
||||||
.where(eq(users_temp.id, result.userId));
|
.where(eq(user.id, result.userId));
|
||||||
|
|
||||||
if (update) {
|
if (update) {
|
||||||
console.log("2FA reset successful");
|
console.log("2FA reset successful");
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ import {
|
|||||||
addNewService,
|
addNewService,
|
||||||
checkServiceAccess,
|
checkServiceAccess,
|
||||||
cloneCompose,
|
cloneCompose,
|
||||||
cloneComposeRemote,
|
|
||||||
createCommand,
|
createCommand,
|
||||||
createCompose,
|
createCompose,
|
||||||
createComposeByTemplate,
|
createComposeByTemplate,
|
||||||
createDomain,
|
createDomain,
|
||||||
createMount,
|
createMount,
|
||||||
deleteMount,
|
deleteMount,
|
||||||
|
execAsync,
|
||||||
|
execAsyncRemote,
|
||||||
findComposeById,
|
findComposeById,
|
||||||
findDomainsByComposeId,
|
findDomainsByComposeId,
|
||||||
findEnvironmentById,
|
findEnvironmentById,
|
||||||
@@ -245,6 +246,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
await cleanQueuesByCompose(input.composeId);
|
await cleanQueuesByCompose(input.composeId);
|
||||||
|
return { success: true, message: "Queues cleaned successfully" };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
loadServices: protectedProcedure
|
loadServices: protectedProcedure
|
||||||
@@ -301,10 +303,12 @@ export const composeRouter = createTRPCRouter({
|
|||||||
message: "You are not authorized to fetch this compose",
|
message: "You are not authorized to fetch this compose",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const command = await cloneCompose(compose);
|
||||||
if (compose.serverId) {
|
if (compose.serverId) {
|
||||||
await cloneComposeRemote(compose);
|
await execAsyncRemote(compose.serverId, command);
|
||||||
} else {
|
} else {
|
||||||
await cloneCompose(compose);
|
await execAsync(command);
|
||||||
}
|
}
|
||||||
return compose.sourceType;
|
return compose.sourceType;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -405,6 +409,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
removeOnFail: true,
|
removeOnFail: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
return { success: true, message: "Deployment queued" };
|
||||||
}),
|
}),
|
||||||
redeploy: protectedProcedure
|
redeploy: protectedProcedure
|
||||||
.input(apiRedeployCompose)
|
.input(apiRedeployCompose)
|
||||||
@@ -440,6 +445,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
removeOnFail: true,
|
removeOnFail: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
return { success: true, message: "Redeployment queued" };
|
||||||
}),
|
}),
|
||||||
stop: protectedProcedure
|
stop: protectedProcedure
|
||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
createDiscordNotification,
|
createDiscordNotification,
|
||||||
createEmailNotification,
|
createEmailNotification,
|
||||||
createLarkNotification,
|
|
||||||
createGotifyNotification,
|
createGotifyNotification,
|
||||||
|
createLarkNotification,
|
||||||
createNtfyNotification,
|
createNtfyNotification,
|
||||||
createSlackNotification,
|
createSlackNotification,
|
||||||
createTelegramNotification,
|
createTelegramNotification,
|
||||||
@@ -11,16 +11,16 @@ import {
|
|||||||
removeNotificationById,
|
removeNotificationById,
|
||||||
sendDiscordNotification,
|
sendDiscordNotification,
|
||||||
sendEmailNotification,
|
sendEmailNotification,
|
||||||
sendLarkNotification,
|
|
||||||
sendGotifyNotification,
|
sendGotifyNotification,
|
||||||
|
sendLarkNotification,
|
||||||
sendNtfyNotification,
|
sendNtfyNotification,
|
||||||
sendServerThresholdNotifications,
|
sendServerThresholdNotifications,
|
||||||
sendSlackNotification,
|
sendSlackNotification,
|
||||||
sendTelegramNotification,
|
sendTelegramNotification,
|
||||||
updateDiscordNotification,
|
updateDiscordNotification,
|
||||||
updateEmailNotification,
|
updateEmailNotification,
|
||||||
updateLarkNotification,
|
|
||||||
updateGotifyNotification,
|
updateGotifyNotification,
|
||||||
|
updateLarkNotification,
|
||||||
updateNtfyNotification,
|
updateNtfyNotification,
|
||||||
updateSlackNotification,
|
updateSlackNotification,
|
||||||
updateTelegramNotification,
|
updateTelegramNotification,
|
||||||
@@ -38,29 +38,29 @@ import { db } from "@/server/db";
|
|||||||
import {
|
import {
|
||||||
apiCreateDiscord,
|
apiCreateDiscord,
|
||||||
apiCreateEmail,
|
apiCreateEmail,
|
||||||
apiCreateLark,
|
|
||||||
apiCreateGotify,
|
apiCreateGotify,
|
||||||
|
apiCreateLark,
|
||||||
apiCreateNtfy,
|
apiCreateNtfy,
|
||||||
apiCreateSlack,
|
apiCreateSlack,
|
||||||
apiCreateTelegram,
|
apiCreateTelegram,
|
||||||
apiFindOneNotification,
|
apiFindOneNotification,
|
||||||
apiTestDiscordConnection,
|
apiTestDiscordConnection,
|
||||||
apiTestEmailConnection,
|
apiTestEmailConnection,
|
||||||
apiTestLarkConnection,
|
|
||||||
apiTestGotifyConnection,
|
apiTestGotifyConnection,
|
||||||
|
apiTestLarkConnection,
|
||||||
apiTestNtfyConnection,
|
apiTestNtfyConnection,
|
||||||
apiTestSlackConnection,
|
apiTestSlackConnection,
|
||||||
apiTestTelegramConnection,
|
apiTestTelegramConnection,
|
||||||
apiUpdateDiscord,
|
apiUpdateDiscord,
|
||||||
apiUpdateEmail,
|
apiUpdateEmail,
|
||||||
apiUpdateLark,
|
|
||||||
apiUpdateGotify,
|
apiUpdateGotify,
|
||||||
|
apiUpdateLark,
|
||||||
apiUpdateNtfy,
|
apiUpdateNtfy,
|
||||||
apiUpdateSlack,
|
apiUpdateSlack,
|
||||||
apiUpdateTelegram,
|
apiUpdateTelegram,
|
||||||
notifications,
|
notifications,
|
||||||
server,
|
server,
|
||||||
users_temp,
|
user,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
|
|
||||||
export const notificationRouter = createTRPCRouter({
|
export const notificationRouter = createTRPCRouter({
|
||||||
@@ -359,9 +359,9 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
if (input.ServerType === "Dokploy") {
|
if (input.ServerType === "Dokploy") {
|
||||||
const result = await db
|
const result = await db
|
||||||
.select()
|
.select()
|
||||||
.from(users_temp)
|
.from(user)
|
||||||
.where(
|
.where(
|
||||||
sql`${users_temp.metricsConfig}::jsonb -> 'server' ->> 'token' = ${input.Token}`,
|
sql`${user.metricsConfig}::jsonb -> 'server' ->> 'token' = ${input.Token}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!result?.[0]?.id) {
|
if (!result?.[0]?.id) {
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ export const organizationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if this is the user's first organization
|
||||||
|
const existingMemberships = await db.query.member.findMany({
|
||||||
|
where: eq(member.userId, ctx.user.id),
|
||||||
|
});
|
||||||
|
|
||||||
await db.insert(member).values({
|
await db.insert(member).values({
|
||||||
organizationId: result.id,
|
organizationId: result.id,
|
||||||
role: "owner",
|
role: "owner",
|
||||||
@@ -63,6 +68,11 @@ export const organizationRouter = createTRPCRouter({
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
with: {
|
||||||
|
members: {
|
||||||
|
where: eq(member.userId, ctx.user.id),
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
return memberResult;
|
return memberResult;
|
||||||
}),
|
}),
|
||||||
@@ -184,4 +194,45 @@ export const organizationRouter = createTRPCRouter({
|
|||||||
.delete(invitation)
|
.delete(invitation)
|
||||||
.where(eq(invitation.id, input.invitationId));
|
.where(eq(invitation.id, input.invitationId));
|
||||||
}),
|
}),
|
||||||
|
setDefault: protectedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
organizationId: z.string().min(1),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
// Verify user is a member of this organization
|
||||||
|
const userMember = await db.query.member.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(member.organizationId, input.organizationId),
|
||||||
|
eq(member.userId, ctx.user.id),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!userMember) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message: "You are not a member of this organization",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// First, unset all defaults for this user
|
||||||
|
await db
|
||||||
|
.update(member)
|
||||||
|
.set({ isDefault: false })
|
||||||
|
.where(eq(member.userId, ctx.user.id));
|
||||||
|
|
||||||
|
// Then set this organization as default
|
||||||
|
await db
|
||||||
|
.update(member)
|
||||||
|
.set({ isDefault: true })
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(member.organizationId, input.organizationId),
|
||||||
|
eq(member.userId, ctx.user.id),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -383,6 +383,15 @@ export const serverRouter = createTRPCRouter({
|
|||||||
const ip = await getPublicIpWithFallback();
|
const ip = await getPublicIpWithFallback();
|
||||||
return ip;
|
return ip;
|
||||||
}),
|
}),
|
||||||
|
getServerTime: protectedProcedure.query(() => {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
time: new Date(),
|
||||||
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||||
|
};
|
||||||
|
}),
|
||||||
getServerMetrics: protectedProcedure
|
getServerMetrics: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
findNotificationById,
|
findNotificationById,
|
||||||
findOrganizationById,
|
findOrganizationById,
|
||||||
findUserById,
|
findUserById,
|
||||||
|
getDokployUrl,
|
||||||
getUserByToken,
|
getUserByToken,
|
||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
removeUserById,
|
removeUserById,
|
||||||
@@ -419,11 +420,10 @@ export const userRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const admin = await findAdmin();
|
|
||||||
const host =
|
const host =
|
||||||
process.env.NODE_ENV === "development"
|
process.env.NODE_ENV === "development"
|
||||||
? "http://localhost:3000"
|
? "http://localhost:3000"
|
||||||
: admin.user.host;
|
: await getDokployUrl();
|
||||||
const inviteLink = `${host}/invitation?token=${input.invitationId}`;
|
const inviteLink = `${host}/invitation?token=${input.invitationId}`;
|
||||||
|
|
||||||
const organization = await findOrganizationById(
|
const organization = await findOrganizationById(
|
||||||
|
|||||||
@@ -2,13 +2,8 @@ import {
|
|||||||
deployApplication,
|
deployApplication,
|
||||||
deployCompose,
|
deployCompose,
|
||||||
deployPreviewApplication,
|
deployPreviewApplication,
|
||||||
deployRemoteApplication,
|
|
||||||
deployRemoteCompose,
|
|
||||||
deployRemotePreviewApplication,
|
|
||||||
rebuildApplication,
|
rebuildApplication,
|
||||||
rebuildCompose,
|
rebuildCompose,
|
||||||
rebuildRemoteApplication,
|
|
||||||
rebuildRemoteCompose,
|
|
||||||
updateApplicationStatus,
|
updateApplicationStatus,
|
||||||
updateCompose,
|
updateCompose,
|
||||||
updatePreviewDeployment,
|
updatePreviewDeployment,
|
||||||
@@ -24,91 +19,48 @@ export const deploymentWorker = new Worker(
|
|||||||
if (job.data.applicationType === "application") {
|
if (job.data.applicationType === "application") {
|
||||||
await updateApplicationStatus(job.data.applicationId, "running");
|
await updateApplicationStatus(job.data.applicationId, "running");
|
||||||
|
|
||||||
if (job.data.server) {
|
if (job.data.type === "redeploy") {
|
||||||
if (job.data.type === "redeploy") {
|
await rebuildApplication({
|
||||||
await rebuildRemoteApplication({
|
applicationId: job.data.applicationId,
|
||||||
applicationId: job.data.applicationId,
|
titleLog: job.data.titleLog,
|
||||||
titleLog: job.data.titleLog,
|
descriptionLog: job.data.descriptionLog,
|
||||||
descriptionLog: job.data.descriptionLog,
|
});
|
||||||
});
|
} else if (job.data.type === "deploy") {
|
||||||
} else if (job.data.type === "deploy") {
|
await deployApplication({
|
||||||
await deployRemoteApplication({
|
applicationId: job.data.applicationId,
|
||||||
applicationId: job.data.applicationId,
|
titleLog: job.data.titleLog,
|
||||||
titleLog: job.data.titleLog,
|
descriptionLog: job.data.descriptionLog,
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if (job.data.applicationType === "compose") {
|
} else if (job.data.applicationType === "compose") {
|
||||||
await updateCompose(job.data.composeId, {
|
await updateCompose(job.data.composeId, {
|
||||||
composeStatus: "running",
|
composeStatus: "running",
|
||||||
});
|
});
|
||||||
|
if (job.data.type === "deploy") {
|
||||||
if (job.data.server) {
|
await deployCompose({
|
||||||
if (job.data.type === "redeploy") {
|
composeId: job.data.composeId,
|
||||||
await rebuildRemoteCompose({
|
titleLog: job.data.titleLog,
|
||||||
composeId: job.data.composeId,
|
descriptionLog: job.data.descriptionLog,
|
||||||
titleLog: job.data.titleLog,
|
});
|
||||||
descriptionLog: job.data.descriptionLog,
|
} else if (job.data.type === "redeploy") {
|
||||||
});
|
await rebuildCompose({
|
||||||
} else if (job.data.type === "deploy") {
|
composeId: job.data.composeId,
|
||||||
await deployRemoteCompose({
|
titleLog: job.data.titleLog,
|
||||||
composeId: job.data.composeId,
|
descriptionLog: job.data.descriptionLog,
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if (job.data.applicationType === "application-preview") {
|
} else if (job.data.applicationType === "application-preview") {
|
||||||
await updatePreviewDeployment(job.data.previewDeploymentId, {
|
await updatePreviewDeployment(job.data.previewDeploymentId, {
|
||||||
previewStatus: "running",
|
previewStatus: "running",
|
||||||
});
|
});
|
||||||
if (job.data.server) {
|
|
||||||
if (job.data.type === "deploy") {
|
if (job.data.type === "deploy") {
|
||||||
await deployRemotePreviewApplication({
|
await deployPreviewApplication({
|
||||||
applicationId: job.data.applicationId,
|
applicationId: job.data.applicationId,
|
||||||
titleLog: job.data.titleLog,
|
titleLog: job.data.titleLog,
|
||||||
descriptionLog: job.data.descriptionLog,
|
descriptionLog: job.data.descriptionLog,
|
||||||
previewDeploymentId: job.data.previewDeploymentId,
|
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type http from "node:http";
|
|||||||
import {
|
import {
|
||||||
docker,
|
docker,
|
||||||
execAsync,
|
execAsync,
|
||||||
|
getHostSystemStats,
|
||||||
getLastAdvancedStatsFile,
|
getLastAdvancedStatsFile,
|
||||||
recordAdvancedStats,
|
recordAdvancedStats,
|
||||||
validateRequest,
|
validateRequest,
|
||||||
@@ -49,6 +50,21 @@ export const setupDockerStatsMonitoringSocketServer = (
|
|||||||
}
|
}
|
||||||
const intervalId = setInterval(async () => {
|
const intervalId = setInterval(async () => {
|
||||||
try {
|
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 = {
|
const filter = {
|
||||||
status: ["running"],
|
status: ["running"],
|
||||||
...(appType === "application" && {
|
...(appType === "application" && {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
await initializeNetwork();
|
await initializeNetwork();
|
||||||
createDefaultTraefikConfig();
|
createDefaultTraefikConfig();
|
||||||
createDefaultServerTraefikConfig();
|
createDefaultServerTraefikConfig();
|
||||||
await execAsync("docker pull traefik:v3.5.0");
|
await execAsync("docker pull traefik:v3.6.1");
|
||||||
await initializeStandaloneTraefik();
|
await initializeStandaloneTraefik();
|
||||||
await initializeRedis();
|
await initializeRedis();
|
||||||
await initializePostgres();
|
await initializePostgres();
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
// boolean,
|
// boolean,
|
||||||
// } from "drizzle-orm/pg-core";
|
// } from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
// export const users_temp = pgTable("users_temp", {
|
// export const user = pgTable("user", {
|
||||||
// id: text("id").primaryKey(),
|
// id: text("id").primaryKey(),
|
||||||
// name: text("name").notNull(),
|
// name: text("name").notNull(),
|
||||||
// email: text("email").notNull().unique(),
|
// email: text("email").notNull().unique(),
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
// userAgent: text("user_agent"),
|
// userAgent: text("user_agent"),
|
||||||
// userId: text("user_id")
|
// userId: text("user_id")
|
||||||
// .notNull()
|
// .notNull()
|
||||||
// .references(() => users_temp.id, { onDelete: "cascade" }),
|
// .references(() => user.id, { onDelete: "cascade" }),
|
||||||
// activeOrganizationId: text("active_organization_id"),
|
// activeOrganizationId: text("active_organization_id"),
|
||||||
// });
|
// });
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
// providerId: text("provider_id").notNull(),
|
// providerId: text("provider_id").notNull(),
|
||||||
// userId: text("user_id")
|
// userId: text("user_id")
|
||||||
// .notNull()
|
// .notNull()
|
||||||
// .references(() => users_temp.id, { onDelete: "cascade" }),
|
// .references(() => user.id, { onDelete: "cascade" }),
|
||||||
// accessToken: text("access_token"),
|
// accessToken: text("access_token"),
|
||||||
// refreshToken: text("refresh_token"),
|
// refreshToken: text("refresh_token"),
|
||||||
// idToken: text("id_token"),
|
// idToken: text("id_token"),
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"micromatch": "4.0.8",
|
"micromatch": "4.0.8",
|
||||||
"nanoid": "3.3.11",
|
"nanoid": "3.3.11",
|
||||||
"node-os-utils": "1.3.7",
|
"node-os-utils": "2.0.1",
|
||||||
"node-pty": "1.0.0",
|
"node-pty": "1.0.0",
|
||||||
"node-schedule": "2.1.1",
|
"node-schedule": "2.1.1",
|
||||||
"nodemailer": "6.9.14",
|
"nodemailer": "6.9.14",
|
||||||
@@ -88,7 +88,6 @@
|
|||||||
"@types/lodash": "4.17.4",
|
"@types/lodash": "4.17.4",
|
||||||
"@types/micromatch": "4.0.9",
|
"@types/micromatch": "4.0.9",
|
||||||
"@types/node": "^18.19.104",
|
"@types/node": "^18.19.104",
|
||||||
"@types/node-os-utils": "1.3.4",
|
|
||||||
"@types/node-schedule": "2.1.6",
|
"@types/node-schedule": "2.1.6",
|
||||||
"@types/nodemailer": "^6.4.17",
|
"@types/nodemailer": "^6.4.17",
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { projects } from "./project";
|
import { projects } from "./project";
|
||||||
import { server } from "./server";
|
import { server } from "./server";
|
||||||
import { users_temp } from "./user";
|
import { user } from "./user";
|
||||||
|
|
||||||
export const account = pgTable("account", {
|
export const account = pgTable("account", {
|
||||||
id: text("id")
|
id: text("id")
|
||||||
@@ -21,7 +21,7 @@ export const account = pgTable("account", {
|
|||||||
providerId: text("provider_id").notNull(),
|
providerId: text("provider_id").notNull(),
|
||||||
userId: text("user_id")
|
userId: text("user_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
.references(() => user.id, { onDelete: "cascade" }),
|
||||||
accessToken: text("access_token"),
|
accessToken: text("access_token"),
|
||||||
refreshToken: text("refresh_token"),
|
refreshToken: text("refresh_token"),
|
||||||
idToken: text("id_token"),
|
idToken: text("id_token"),
|
||||||
@@ -39,9 +39,9 @@ export const account = pgTable("account", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const accountRelations = relations(account, ({ one }) => ({
|
export const accountRelations = relations(account, ({ one }) => ({
|
||||||
user: one(users_temp, {
|
user: one(user, {
|
||||||
fields: [account.userId],
|
fields: [account.userId],
|
||||||
references: [users_temp.id],
|
references: [user.id],
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -65,15 +65,15 @@ export const organization = pgTable("organization", {
|
|||||||
metadata: text("metadata"),
|
metadata: text("metadata"),
|
||||||
ownerId: text("owner_id")
|
ownerId: text("owner_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
.references(() => user.id, { onDelete: "cascade" }),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const organizationRelations = relations(
|
export const organizationRelations = relations(
|
||||||
organization,
|
organization,
|
||||||
({ one, many }) => ({
|
({ one, many }) => ({
|
||||||
owner: one(users_temp, {
|
owner: one(user, {
|
||||||
fields: [organization.ownerId],
|
fields: [organization.ownerId],
|
||||||
references: [users_temp.id],
|
references: [user.id],
|
||||||
}),
|
}),
|
||||||
servers: many(server),
|
servers: many(server),
|
||||||
projects: many(projects),
|
projects: many(projects),
|
||||||
@@ -90,10 +90,11 @@ export const member = pgTable("member", {
|
|||||||
.references(() => organization.id, { onDelete: "cascade" }),
|
.references(() => organization.id, { onDelete: "cascade" }),
|
||||||
userId: text("user_id")
|
userId: text("user_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
.references(() => user.id, { onDelete: "cascade" }),
|
||||||
role: text("role").notNull().$type<"owner" | "member" | "admin">(),
|
role: text("role").notNull().$type<"owner" | "member" | "admin">(),
|
||||||
createdAt: timestamp("created_at").notNull(),
|
createdAt: timestamp("created_at").notNull(),
|
||||||
teamId: text("team_id"),
|
teamId: text("team_id"),
|
||||||
|
isDefault: boolean("is_default").notNull().default(false),
|
||||||
// Permissions
|
// Permissions
|
||||||
canCreateProjects: boolean("canCreateProjects").notNull().default(false),
|
canCreateProjects: boolean("canCreateProjects").notNull().default(false),
|
||||||
canAccessToSSHKeys: boolean("canAccessToSSHKeys").notNull().default(false),
|
canAccessToSSHKeys: boolean("canAccessToSSHKeys").notNull().default(false),
|
||||||
@@ -133,9 +134,9 @@ export const memberRelations = relations(member, ({ one }) => ({
|
|||||||
fields: [member.organizationId],
|
fields: [member.organizationId],
|
||||||
references: [organization.id],
|
references: [organization.id],
|
||||||
}),
|
}),
|
||||||
user: one(users_temp, {
|
user: one(user, {
|
||||||
fields: [member.userId],
|
fields: [member.userId],
|
||||||
references: [users_temp.id],
|
references: [user.id],
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -150,7 +151,7 @@ export const invitation = pgTable("invitation", {
|
|||||||
expiresAt: timestamp("expires_at").notNull(),
|
expiresAt: timestamp("expires_at").notNull(),
|
||||||
inviterId: text("inviter_id")
|
inviterId: text("inviter_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
.references(() => user.id, { onDelete: "cascade" }),
|
||||||
teamId: text("team_id"),
|
teamId: text("team_id"),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -167,7 +168,7 @@ export const twoFactor = pgTable("two_factor", {
|
|||||||
backupCodes: text("backup_codes").notNull(),
|
backupCodes: text("backup_codes").notNull(),
|
||||||
userId: text("user_id")
|
userId: text("user_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
.references(() => user.id, { onDelete: "cascade" }),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apikey = pgTable("apikey", {
|
export const apikey = pgTable("apikey", {
|
||||||
@@ -178,7 +179,7 @@ export const apikey = pgTable("apikey", {
|
|||||||
key: text("key").notNull(),
|
key: text("key").notNull(),
|
||||||
userId: text("user_id")
|
userId: text("user_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
.references(() => user.id, { onDelete: "cascade" }),
|
||||||
refillInterval: integer("refill_interval"),
|
refillInterval: integer("refill_interval"),
|
||||||
refillAmount: integer("refill_amount"),
|
refillAmount: integer("refill_amount"),
|
||||||
lastRefillAt: timestamp("last_refill_at"),
|
lastRefillAt: timestamp("last_refill_at"),
|
||||||
@@ -197,8 +198,8 @@ export const apikey = pgTable("apikey", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const apikeyRelations = relations(apikey, ({ one }) => ({
|
export const apikeyRelations = relations(apikey, ({ one }) => ({
|
||||||
user: one(users_temp, {
|
user: one(user, {
|
||||||
fields: [apikey.userId],
|
fields: [apikey.userId],
|
||||||
references: [users_temp.id],
|
references: [user.id],
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ import { server } from "./server";
|
|||||||
import {
|
import {
|
||||||
applicationStatus,
|
applicationStatus,
|
||||||
certificateType,
|
certificateType,
|
||||||
|
type EndpointSpecSwarm,
|
||||||
|
EndpointSpecSwarmSchema,
|
||||||
type HealthCheckSwarm,
|
type HealthCheckSwarm,
|
||||||
HealthCheckSwarmSchema,
|
HealthCheckSwarmSchema,
|
||||||
type LabelsSwarm,
|
type LabelsSwarm,
|
||||||
@@ -167,6 +169,7 @@ export const applications = pgTable("application", {
|
|||||||
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
|
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
|
||||||
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
|
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
|
||||||
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
|
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
|
||||||
|
endpointSpecSwarm: json("endpointSpecSwarm").$type<EndpointSpecSwarm>(),
|
||||||
//
|
//
|
||||||
replicas: integer("replicas").default(1).notNull(),
|
replicas: integer("replicas").default(1).notNull(),
|
||||||
applicationStatus: applicationStatus("applicationStatus")
|
applicationStatus: applicationStatus("applicationStatus")
|
||||||
@@ -318,6 +321,7 @@ const createSchema = createInsertSchema(applications, {
|
|||||||
previewLabels: z.array(z.string()).optional(),
|
previewLabels: z.array(z.string()).optional(),
|
||||||
cleanCache: z.boolean().optional(),
|
cleanCache: z.boolean().optional(),
|
||||||
stopGracePeriodSwarm: z.bigint().nullable(),
|
stopGracePeriodSwarm: z.bigint().nullable(),
|
||||||
|
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apiCreateApplication = createSchema.pick({
|
export const apiCreateApplication = createSchema.pick({
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { mariadb } from "./mariadb";
|
|||||||
import { mongo } from "./mongo";
|
import { mongo } from "./mongo";
|
||||||
import { mysql } from "./mysql";
|
import { mysql } from "./mysql";
|
||||||
import { postgres } from "./postgres";
|
import { postgres } from "./postgres";
|
||||||
import { users_temp } from "./user";
|
import { user } from "./user";
|
||||||
export const databaseType = pgEnum("databaseType", [
|
export const databaseType = pgEnum("databaseType", [
|
||||||
"postgres",
|
"postgres",
|
||||||
"mariadb",
|
"mariadb",
|
||||||
@@ -74,7 +74,7 @@ export const backups = pgTable("backup", {
|
|||||||
mongoId: text("mongoId").references((): AnyPgColumn => mongo.mongoId, {
|
mongoId: text("mongoId").references((): AnyPgColumn => mongo.mongoId, {
|
||||||
onDelete: "cascade",
|
onDelete: "cascade",
|
||||||
}),
|
}),
|
||||||
userId: text("userId").references(() => users_temp.id),
|
userId: text("userId").references(() => user.id),
|
||||||
// Only for compose backups
|
// Only for compose backups
|
||||||
metadata: jsonb("metadata").$type<
|
metadata: jsonb("metadata").$type<
|
||||||
| {
|
| {
|
||||||
@@ -118,9 +118,9 @@ export const backupsRelations = relations(backups, ({ one, many }) => ({
|
|||||||
fields: [backups.mongoId],
|
fields: [backups.mongoId],
|
||||||
references: [mongo.mongoId],
|
references: [mongo.mongoId],
|
||||||
}),
|
}),
|
||||||
user: one(users_temp, {
|
user: one(user, {
|
||||||
fields: [backups.userId],
|
fields: [backups.userId],
|
||||||
references: [users_temp.id],
|
references: [user.id],
|
||||||
}),
|
}),
|
||||||
compose: one(compose, {
|
compose: one(compose, {
|
||||||
fields: [backups.composeId],
|
fields: [backups.composeId],
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { bitbucket } from "./bitbucket";
|
|||||||
import { gitea } from "./gitea";
|
import { gitea } from "./gitea";
|
||||||
import { github } from "./github";
|
import { github } from "./github";
|
||||||
import { gitlab } from "./gitlab";
|
import { gitlab } from "./gitlab";
|
||||||
import { users_temp } from "./user";
|
import { user } from "./user";
|
||||||
|
|
||||||
export const gitProviderType = pgEnum("gitProviderType", [
|
export const gitProviderType = pgEnum("gitProviderType", [
|
||||||
"github",
|
"github",
|
||||||
@@ -32,7 +32,7 @@ export const gitProvider = pgTable("git_provider", {
|
|||||||
.references(() => organization.id, { onDelete: "cascade" }),
|
.references(() => organization.id, { onDelete: "cascade" }),
|
||||||
userId: text("userId")
|
userId: text("userId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
.references(() => user.id, { onDelete: "cascade" }),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const gitProviderRelations = relations(gitProvider, ({ one }) => ({
|
export const gitProviderRelations = relations(gitProvider, ({ one }) => ({
|
||||||
@@ -56,9 +56,9 @@ export const gitProviderRelations = relations(gitProvider, ({ one }) => ({
|
|||||||
fields: [gitProvider.organizationId],
|
fields: [gitProvider.organizationId],
|
||||||
references: [organization.id],
|
references: [organization.id],
|
||||||
}),
|
}),
|
||||||
user: one(users_temp, {
|
user: one(user, {
|
||||||
fields: [gitProvider.userId],
|
fields: [gitProvider.userId],
|
||||||
references: [users_temp.id],
|
references: [user.id],
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { mounts } from "./mount";
|
|||||||
import { server } from "./server";
|
import { server } from "./server";
|
||||||
import {
|
import {
|
||||||
applicationStatus,
|
applicationStatus,
|
||||||
|
type EndpointSpecSwarm,
|
||||||
|
EndpointSpecSwarmSchema,
|
||||||
type HealthCheckSwarm,
|
type HealthCheckSwarm,
|
||||||
HealthCheckSwarmSchema,
|
HealthCheckSwarmSchema,
|
||||||
type LabelsSwarm,
|
type LabelsSwarm,
|
||||||
@@ -63,6 +65,7 @@ export const mariadb = pgTable("mariadb", {
|
|||||||
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
|
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
|
||||||
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
|
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
|
||||||
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
|
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
|
||||||
|
endpointSpecSwarm: json("endpointSpecSwarm").$type<EndpointSpecSwarm>(),
|
||||||
replicas: integer("replicas").default(1).notNull(),
|
replicas: integer("replicas").default(1).notNull(),
|
||||||
createdAt: text("createdAt")
|
createdAt: text("createdAt")
|
||||||
.notNull()
|
.notNull()
|
||||||
@@ -130,6 +133,7 @@ const createSchema = createInsertSchema(mariadb, {
|
|||||||
labelsSwarm: LabelsSwarmSchema.nullable(),
|
labelsSwarm: LabelsSwarmSchema.nullable(),
|
||||||
networkSwarm: NetworkSwarmSchema.nullable(),
|
networkSwarm: NetworkSwarmSchema.nullable(),
|
||||||
stopGracePeriodSwarm: z.bigint().nullable(),
|
stopGracePeriodSwarm: z.bigint().nullable(),
|
||||||
|
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apiCreateMariaDB = createSchema
|
export const apiCreateMariaDB = createSchema
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import { mounts } from "./mount";
|
|||||||
import { server } from "./server";
|
import { server } from "./server";
|
||||||
import {
|
import {
|
||||||
applicationStatus,
|
applicationStatus,
|
||||||
|
type EndpointSpecSwarm,
|
||||||
|
EndpointSpecSwarmSchema,
|
||||||
type HealthCheckSwarm,
|
type HealthCheckSwarm,
|
||||||
HealthCheckSwarmSchema,
|
HealthCheckSwarmSchema,
|
||||||
type LabelsSwarm,
|
type LabelsSwarm,
|
||||||
@@ -66,6 +68,7 @@ export const mongo = pgTable("mongo", {
|
|||||||
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
|
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
|
||||||
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
|
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
|
||||||
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
|
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
|
||||||
|
endpointSpecSwarm: json("endpointSpecSwarm").$type<EndpointSpecSwarm>(),
|
||||||
replicas: integer("replicas").default(1).notNull(),
|
replicas: integer("replicas").default(1).notNull(),
|
||||||
createdAt: text("createdAt")
|
createdAt: text("createdAt")
|
||||||
.notNull()
|
.notNull()
|
||||||
@@ -127,6 +130,7 @@ const createSchema = createInsertSchema(mongo, {
|
|||||||
labelsSwarm: LabelsSwarmSchema.nullable(),
|
labelsSwarm: LabelsSwarmSchema.nullable(),
|
||||||
networkSwarm: NetworkSwarmSchema.nullable(),
|
networkSwarm: NetworkSwarmSchema.nullable(),
|
||||||
stopGracePeriodSwarm: z.bigint().nullable(),
|
stopGracePeriodSwarm: z.bigint().nullable(),
|
||||||
|
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apiCreateMongo = createSchema
|
export const apiCreateMongo = createSchema
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { mounts } from "./mount";
|
|||||||
import { server } from "./server";
|
import { server } from "./server";
|
||||||
import {
|
import {
|
||||||
applicationStatus,
|
applicationStatus,
|
||||||
|
type EndpointSpecSwarm,
|
||||||
|
EndpointSpecSwarmSchema,
|
||||||
type HealthCheckSwarm,
|
type HealthCheckSwarm,
|
||||||
HealthCheckSwarmSchema,
|
HealthCheckSwarmSchema,
|
||||||
type LabelsSwarm,
|
type LabelsSwarm,
|
||||||
@@ -61,6 +63,7 @@ export const mysql = pgTable("mysql", {
|
|||||||
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
|
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
|
||||||
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
|
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
|
||||||
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
|
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
|
||||||
|
endpointSpecSwarm: json("endpointSpecSwarm").$type<EndpointSpecSwarm>(),
|
||||||
replicas: integer("replicas").default(1).notNull(),
|
replicas: integer("replicas").default(1).notNull(),
|
||||||
createdAt: text("createdAt")
|
createdAt: text("createdAt")
|
||||||
.notNull()
|
.notNull()
|
||||||
@@ -127,6 +130,7 @@ const createSchema = createInsertSchema(mysql, {
|
|||||||
labelsSwarm: LabelsSwarmSchema.nullable(),
|
labelsSwarm: LabelsSwarmSchema.nullable(),
|
||||||
networkSwarm: NetworkSwarmSchema.nullable(),
|
networkSwarm: NetworkSwarmSchema.nullable(),
|
||||||
stopGracePeriodSwarm: z.bigint().nullable(),
|
stopGracePeriodSwarm: z.bigint().nullable(),
|
||||||
|
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apiCreateMySql = createSchema
|
export const apiCreateMySql = createSchema
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { mounts } from "./mount";
|
|||||||
import { server } from "./server";
|
import { server } from "./server";
|
||||||
import {
|
import {
|
||||||
applicationStatus,
|
applicationStatus,
|
||||||
|
type EndpointSpecSwarm,
|
||||||
|
EndpointSpecSwarmSchema,
|
||||||
type HealthCheckSwarm,
|
type HealthCheckSwarm,
|
||||||
HealthCheckSwarmSchema,
|
HealthCheckSwarmSchema,
|
||||||
type LabelsSwarm,
|
type LabelsSwarm,
|
||||||
@@ -61,6 +63,7 @@ export const postgres = pgTable("postgres", {
|
|||||||
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
|
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
|
||||||
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
|
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
|
||||||
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
|
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
|
||||||
|
endpointSpecSwarm: json("endpointSpecSwarm").$type<EndpointSpecSwarm>(),
|
||||||
replicas: integer("replicas").default(1).notNull(),
|
replicas: integer("replicas").default(1).notNull(),
|
||||||
createdAt: text("createdAt")
|
createdAt: text("createdAt")
|
||||||
.notNull()
|
.notNull()
|
||||||
@@ -120,6 +123,7 @@ const createSchema = createInsertSchema(postgres, {
|
|||||||
labelsSwarm: LabelsSwarmSchema.nullable(),
|
labelsSwarm: LabelsSwarmSchema.nullable(),
|
||||||
networkSwarm: NetworkSwarmSchema.nullable(),
|
networkSwarm: NetworkSwarmSchema.nullable(),
|
||||||
stopGracePeriodSwarm: z.bigint().nullable(),
|
stopGracePeriodSwarm: z.bigint().nullable(),
|
||||||
|
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apiCreatePostgres = createSchema
|
export const apiCreatePostgres = createSchema
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ import { nanoid } from "nanoid";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { environments } from "./environment";
|
import { environments } from "./environment";
|
||||||
import { mounts } from "./mount";
|
import { mounts } from "./mount";
|
||||||
import { projects } from "./project";
|
|
||||||
import { server } from "./server";
|
import { server } from "./server";
|
||||||
import {
|
import {
|
||||||
applicationStatus,
|
applicationStatus,
|
||||||
|
type EndpointSpecSwarm,
|
||||||
|
EndpointSpecSwarmSchema,
|
||||||
type HealthCheckSwarm,
|
type HealthCheckSwarm,
|
||||||
HealthCheckSwarmSchema,
|
HealthCheckSwarmSchema,
|
||||||
type LabelsSwarm,
|
type LabelsSwarm,
|
||||||
@@ -61,6 +62,7 @@ export const redis = pgTable("redis", {
|
|||||||
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
|
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
|
||||||
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
|
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
|
||||||
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
|
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
|
||||||
|
endpointSpecSwarm: json("endpointSpecSwarm").$type<EndpointSpecSwarm>(),
|
||||||
replicas: integer("replicas").default(1).notNull(),
|
replicas: integer("replicas").default(1).notNull(),
|
||||||
|
|
||||||
environmentId: text("environmentId")
|
environmentId: text("environmentId")
|
||||||
@@ -110,6 +112,7 @@ const createSchema = createInsertSchema(redis, {
|
|||||||
labelsSwarm: LabelsSwarmSchema.nullable(),
|
labelsSwarm: LabelsSwarmSchema.nullable(),
|
||||||
networkSwarm: NetworkSwarmSchema.nullable(),
|
networkSwarm: NetworkSwarmSchema.nullable(),
|
||||||
stopGracePeriodSwarm: z.bigint().nullable(),
|
stopGracePeriodSwarm: z.bigint().nullable(),
|
||||||
|
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apiCreateRedis = createSchema
|
export const apiCreateRedis = createSchema
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { applications } from "./application";
|
|||||||
import { compose } from "./compose";
|
import { compose } from "./compose";
|
||||||
import { deployments } from "./deployment";
|
import { deployments } from "./deployment";
|
||||||
import { server } from "./server";
|
import { server } from "./server";
|
||||||
import { users_temp } from "./user";
|
import { user } from "./user";
|
||||||
import { generateAppName } from "./utils";
|
import { generateAppName } from "./utils";
|
||||||
export const shellTypes = pgEnum("shellType", ["bash", "sh"]);
|
export const shellTypes = pgEnum("shellType", ["bash", "sh"]);
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ export const schedules = pgTable("schedule", {
|
|||||||
serverId: text("serverId").references(() => server.serverId, {
|
serverId: text("serverId").references(() => server.serverId, {
|
||||||
onDelete: "cascade",
|
onDelete: "cascade",
|
||||||
}),
|
}),
|
||||||
userId: text("userId").references(() => users_temp.id, {
|
userId: text("userId").references(() => user.id, {
|
||||||
onDelete: "cascade",
|
onDelete: "cascade",
|
||||||
}),
|
}),
|
||||||
enabled: boolean("enabled").notNull().default(true),
|
enabled: boolean("enabled").notNull().default(true),
|
||||||
@@ -69,9 +69,9 @@ export const schedulesRelations = relations(schedules, ({ one, many }) => ({
|
|||||||
fields: [schedules.serverId],
|
fields: [schedules.serverId],
|
||||||
references: [server.serverId],
|
references: [server.serverId],
|
||||||
}),
|
}),
|
||||||
user: one(users_temp, {
|
user: one(user, {
|
||||||
fields: [schedules.userId],
|
fields: [schedules.userId],
|
||||||
references: [users_temp.id],
|
references: [user.id],
|
||||||
}),
|
}),
|
||||||
deployments: many(deployments),
|
deployments: many(deployments),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
||||||
import { users_temp } from "./user";
|
import { user } from "./user";
|
||||||
|
|
||||||
// OLD TABLE
|
// OLD TABLE
|
||||||
export const session = pgTable("session_temp", {
|
export const session = pgTable("session_temp", {
|
||||||
@@ -12,7 +12,7 @@ export const session = pgTable("session_temp", {
|
|||||||
userAgent: text("user_agent"),
|
userAgent: text("user_agent"),
|
||||||
userId: text("user_id")
|
userId: text("user_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
.references(() => user.id, { onDelete: "cascade" }),
|
||||||
impersonatedBy: text("impersonated_by"),
|
impersonatedBy: text("impersonated_by"),
|
||||||
activeOrganizationId: text("active_organization_id"),
|
activeOrganizationId: text("active_organization_id"),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -74,6 +74,18 @@ export interface LabelsSwarm {
|
|||||||
[name: string]: string;
|
[name: string]: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface EndpointPortConfigSwarm {
|
||||||
|
Protocol?: string | undefined;
|
||||||
|
TargetPort?: number | undefined;
|
||||||
|
PublishedPort?: number | undefined;
|
||||||
|
PublishMode?: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EndpointSpecSwarm {
|
||||||
|
Mode?: string | undefined;
|
||||||
|
Ports?: EndpointPortConfigSwarm[] | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export const HealthCheckSwarmSchema = z
|
export const HealthCheckSwarmSchema = z
|
||||||
.object({
|
.object({
|
||||||
Test: z.array(z.string()).optional(),
|
Test: z.array(z.string()).optional(),
|
||||||
@@ -161,3 +173,19 @@ export const NetworkSwarmSchema = z.array(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const LabelsSwarmSchema = z.record(z.string());
|
export const LabelsSwarmSchema = z.record(z.string());
|
||||||
|
|
||||||
|
export const EndpointPortConfigSwarmSchema = z
|
||||||
|
.object({
|
||||||
|
Protocol: z.string().optional(),
|
||||||
|
TargetPort: z.number().optional(),
|
||||||
|
PublishedPort: z.number().optional(),
|
||||||
|
PublishMode: z.string().optional(),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
export const EndpointSpecSwarmSchema = z
|
||||||
|
.object({
|
||||||
|
Mode: z.string().optional(),
|
||||||
|
Ports: z.array(EndpointPortConfigSwarmSchema).optional(),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import { certificateType } from "./shared";
|
|||||||
// OLD TABLE
|
// OLD TABLE
|
||||||
|
|
||||||
// TEMP
|
// TEMP
|
||||||
export const users_temp = pgTable("user_temp", {
|
export const user = pgTable("user", {
|
||||||
id: text("id")
|
id: text("id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.primaryKey()
|
.primaryKey()
|
||||||
@@ -122,9 +122,9 @@ export const users_temp = pgTable("user_temp", {
|
|||||||
serversQuantity: integer("serversQuantity").notNull().default(0),
|
serversQuantity: integer("serversQuantity").notNull().default(0),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const usersRelations = relations(users_temp, ({ one, many }) => ({
|
export const usersRelations = relations(user, ({ one, many }) => ({
|
||||||
account: one(account, {
|
account: one(account, {
|
||||||
fields: [users_temp.id],
|
fields: [user.id],
|
||||||
references: [account.userId],
|
references: [account.userId],
|
||||||
}),
|
}),
|
||||||
organizations: many(organization),
|
organizations: many(organization),
|
||||||
@@ -134,7 +134,7 @@ export const usersRelations = relations(users_temp, ({ one, many }) => ({
|
|||||||
schedules: many(schedules),
|
schedules: many(schedules),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const createSchema = createInsertSchema(users_temp, {
|
const createSchema = createInsertSchema(user, {
|
||||||
id: z.string().min(1),
|
id: z.string().min(1),
|
||||||
isRegistered: z.boolean().optional(),
|
isRegistered: z.boolean().optional(),
|
||||||
}).omit({
|
}).omit({
|
||||||
|
|||||||
@@ -165,6 +165,7 @@ const { handler, api } = betterAuth({
|
|||||||
organizationId: organization?.id || "",
|
organizationId: organization?.id || "",
|
||||||
role: "owner",
|
role: "owner",
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
|
isDefault: true, // Mark first organization as default
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -174,9 +175,14 @@ const { handler, api } = betterAuth({
|
|||||||
session: {
|
session: {
|
||||||
create: {
|
create: {
|
||||||
before: async (session) => {
|
before: async (session) => {
|
||||||
|
// Find the default organization for this user
|
||||||
|
// Priority: 1) isDefault=true, 2) most recently created
|
||||||
const member = await db.query.member.findFirst({
|
const member = await db.query.member.findFirst({
|
||||||
where: eq(schema.member.userId, session.userId),
|
where: eq(schema.member.userId, session.userId),
|
||||||
orderBy: desc(schema.member.createdAt),
|
orderBy: [
|
||||||
|
desc(schema.member.isDefault),
|
||||||
|
desc(schema.member.createdAt),
|
||||||
|
],
|
||||||
with: {
|
with: {
|
||||||
organization: true,
|
organization: true,
|
||||||
},
|
},
|
||||||
@@ -197,7 +203,7 @@ const { handler, api } = betterAuth({
|
|||||||
updateAge: 60 * 60 * 24,
|
updateAge: 60 * 60 * 24,
|
||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
modelName: "users_temp",
|
modelName: "user",
|
||||||
additionalFields: {
|
additionalFields: {
|
||||||
role: {
|
role: {
|
||||||
type: "string",
|
type: "string",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { promises } from "node:fs";
|
import { promises } from "node:fs";
|
||||||
import osUtils from "node-os-utils";
|
import { OSUtils } from "node-os-utils";
|
||||||
import { paths } from "../constants";
|
import { paths } from "../constants";
|
||||||
|
|
||||||
export interface Container {
|
export interface Container {
|
||||||
@@ -38,22 +38,122 @@ export const recordAdvancedStats = async (
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (appName === "dokploy") {
|
if (appName === "dokploy") {
|
||||||
const disk = await osUtils.drive.info("/");
|
const osutils = new OSUtils();
|
||||||
|
const diskResult = await osutils.disk.usageByMountPoint("/");
|
||||||
|
|
||||||
const diskUsage = disk.usedGb;
|
if (diskResult.success && diskResult.data) {
|
||||||
const diskTotal = disk.totalGb;
|
const disk = diskResult.data;
|
||||||
const diskUsedPercentage = disk.usedPercentage;
|
const diskUsage = disk.used.toGB().toFixed(2);
|
||||||
const diskFree = disk.freeGb;
|
const diskTotal = disk.total.toGB().toFixed(2);
|
||||||
|
const diskUsedPercentage = disk.usagePercentage;
|
||||||
|
const diskFree = disk.available.toGB().toFixed(2);
|
||||||
|
|
||||||
await updateStatsFile(appName, "disk", {
|
await updateStatsFile(appName, "disk", {
|
||||||
diskTotal: +diskTotal,
|
diskTotal: +diskTotal,
|
||||||
diskUsedPercentage: +diskUsedPercentage,
|
diskUsedPercentage: +diskUsedPercentage,
|
||||||
diskUsage: +diskUsage,
|
diskUsage: +diskUsage,
|
||||||
diskFree: +diskFree,
|
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) => {
|
export const getAdvancedStats = async (appName: string) => {
|
||||||
return {
|
return {
|
||||||
cpu: await readStatsFile(appName, "cpu"),
|
cpu: await readStatsFile(appName, "cpu"),
|
||||||
|
|||||||
@@ -3,26 +3,26 @@ import {
|
|||||||
invitation,
|
invitation,
|
||||||
member,
|
member,
|
||||||
organization,
|
organization,
|
||||||
users_temp,
|
user,
|
||||||
} from "@dokploy/server/db/schema";
|
} from "@dokploy/server/db/schema";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { IS_CLOUD } from "../constants";
|
import { IS_CLOUD } from "../constants";
|
||||||
|
|
||||||
export const findUserById = async (userId: string) => {
|
export const findUserById = async (userId: string) => {
|
||||||
const user = await db.query.users_temp.findFirst({
|
const userResult = await db.query.user.findFirst({
|
||||||
where: eq(users_temp.id, userId),
|
where: eq(user.id, userId),
|
||||||
// with: {
|
// with: {
|
||||||
// account: true,
|
// account: true,
|
||||||
// },
|
// },
|
||||||
});
|
});
|
||||||
if (!user) {
|
if (!userResult) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "NOT_FOUND",
|
code: "NOT_FOUND",
|
||||||
message: "User not found",
|
message: "User not found",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return user;
|
return userResult;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const findOrganizationById = async (organizationId: string) => {
|
export const findOrganizationById = async (organizationId: string) => {
|
||||||
@@ -64,7 +64,7 @@ export const findAdmin = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getUserByToken = async (token: string) => {
|
export const getUserByToken = async (token: string) => {
|
||||||
const user = await db.query.invitation.findFirst({
|
const userResult = await db.query.invitation.findFirst({
|
||||||
where: eq(invitation.id, token),
|
where: eq(invitation.id, token),
|
||||||
columns: {
|
columns: {
|
||||||
id: true,
|
id: true,
|
||||||
@@ -76,29 +76,29 @@ export const getUserByToken = async (token: string) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!userResult) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "NOT_FOUND",
|
code: "NOT_FOUND",
|
||||||
message: "Invitation not found",
|
message: "Invitation not found",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const userAlreadyExists = await db.query.users_temp.findFirst({
|
const userAlreadyExists = await db.query.user.findFirst({
|
||||||
where: eq(users_temp.email, user?.email || ""),
|
where: eq(user.email, userResult?.email || ""),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { expiresAt, ...rest } = user;
|
const { expiresAt, ...rest } = userResult;
|
||||||
return {
|
return {
|
||||||
...rest,
|
...rest,
|
||||||
isExpired: user.expiresAt < new Date(),
|
isExpired: userResult.expiresAt < new Date(),
|
||||||
userAlreadyExists: !!userAlreadyExists,
|
userAlreadyExists: !!userAlreadyExists,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const removeUserById = async (userId: string) => {
|
export const removeUserById = async (userId: string) => {
|
||||||
await db
|
await db
|
||||||
.delete(users_temp)
|
.delete(user)
|
||||||
.where(eq(users_temp.id, userId))
|
.where(eq(user.id, userId))
|
||||||
.returning()
|
.returning()
|
||||||
.then((res) => res[0]);
|
.then((res) => res[0]);
|
||||||
};
|
};
|
||||||
@@ -110,7 +110,8 @@ export const getDokployUrl = async () => {
|
|||||||
const admin = await findAdmin();
|
const admin = await findAdmin();
|
||||||
|
|
||||||
if (admin.user.host) {
|
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}`;
|
return `http://${admin.user.serverIp}:${process.env.PORT}`;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,45 +7,32 @@ import {
|
|||||||
} from "@dokploy/server/db/schema";
|
} from "@dokploy/server/db/schema";
|
||||||
import { getAdvancedStats } from "@dokploy/server/monitoring/utils";
|
import { getAdvancedStats } from "@dokploy/server/monitoring/utils";
|
||||||
import {
|
import {
|
||||||
buildApplication,
|
|
||||||
getBuildCommand,
|
getBuildCommand,
|
||||||
mechanizeDockerContainer,
|
mechanizeDockerContainer,
|
||||||
} from "@dokploy/server/utils/builders";
|
} from "@dokploy/server/utils/builders";
|
||||||
import { sendBuildErrorNotifications } from "@dokploy/server/utils/notifications/build-error";
|
import { sendBuildErrorNotifications } from "@dokploy/server/utils/notifications/build-error";
|
||||||
import { sendBuildSuccessNotifications } from "@dokploy/server/utils/notifications/build-success";
|
import { sendBuildSuccessNotifications } from "@dokploy/server/utils/notifications/build-success";
|
||||||
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
|
|
||||||
import {
|
import {
|
||||||
cloneBitbucketRepository,
|
execAsync,
|
||||||
getBitbucketCloneCommand,
|
execAsyncRemote,
|
||||||
} from "@dokploy/server/utils/providers/bitbucket";
|
} from "@dokploy/server/utils/process/execAsync";
|
||||||
import {
|
import { cloneBitbucketRepository } from "@dokploy/server/utils/providers/bitbucket";
|
||||||
buildDocker,
|
import { buildRemoteDocker } from "@dokploy/server/utils/providers/docker";
|
||||||
buildRemoteDocker,
|
|
||||||
} from "@dokploy/server/utils/providers/docker";
|
|
||||||
import {
|
import {
|
||||||
cloneGitRepository,
|
cloneGitRepository,
|
||||||
getCustomGitCloneCommand,
|
getGitCommitInfo,
|
||||||
} from "@dokploy/server/utils/providers/git";
|
} from "@dokploy/server/utils/providers/git";
|
||||||
import {
|
import { cloneGiteaRepository } from "@dokploy/server/utils/providers/gitea";
|
||||||
cloneGiteaRepository,
|
import { cloneGithubRepository } from "@dokploy/server/utils/providers/github";
|
||||||
getGiteaCloneCommand,
|
import { cloneGitlabRepository } from "@dokploy/server/utils/providers/gitlab";
|
||||||
} 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 { createTraefikConfig } from "@dokploy/server/utils/traefik/application";
|
import { createTraefikConfig } from "@dokploy/server/utils/traefik/application";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { encodeBase64 } from "../utils/docker/utils";
|
|
||||||
import { getDokployUrl } from "./admin";
|
import { getDokployUrl } from "./admin";
|
||||||
import {
|
import {
|
||||||
createDeployment,
|
createDeployment,
|
||||||
createDeploymentPreview,
|
createDeploymentPreview,
|
||||||
|
updateDeployment,
|
||||||
updateDeploymentStatus,
|
updateDeploymentStatus,
|
||||||
} from "./deployment";
|
} from "./deployment";
|
||||||
import { type Domain, getDomainHost } from "./domain";
|
import { type Domain, getDomainHost } from "./domain";
|
||||||
@@ -192,30 +179,31 @@ export const deployApplication = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
let command = "set -e;";
|
||||||
if (application.sourceType === "github") {
|
if (application.sourceType === "github") {
|
||||||
await cloneGithubRepository({
|
command += await cloneGithubRepository(application);
|
||||||
...application,
|
|
||||||
logPath: deployment.logPath,
|
|
||||||
});
|
|
||||||
await buildApplication(application, deployment.logPath);
|
|
||||||
} else if (application.sourceType === "gitlab") {
|
} else if (application.sourceType === "gitlab") {
|
||||||
await cloneGitlabRepository(application, deployment.logPath);
|
command += await cloneGitlabRepository(application);
|
||||||
await buildApplication(application, deployment.logPath);
|
|
||||||
} else if (application.sourceType === "gitea") {
|
} else if (application.sourceType === "gitea") {
|
||||||
await cloneGiteaRepository(application, deployment.logPath);
|
command += await cloneGiteaRepository(application);
|
||||||
await buildApplication(application, deployment.logPath);
|
|
||||||
} else if (application.sourceType === "bitbucket") {
|
} else if (application.sourceType === "bitbucket") {
|
||||||
await cloneBitbucketRepository(application, deployment.logPath);
|
command += await cloneBitbucketRepository(application);
|
||||||
await buildApplication(application, deployment.logPath);
|
|
||||||
} else if (application.sourceType === "docker") {
|
|
||||||
await buildDocker(application, deployment.logPath);
|
|
||||||
} else if (application.sourceType === "git") {
|
} else if (application.sourceType === "git") {
|
||||||
await cloneGitRepository(application, deployment.logPath);
|
command += await cloneGitRepository(application);
|
||||||
await buildApplication(application, deployment.logPath);
|
} else if (application.sourceType === "docker") {
|
||||||
} else if (application.sourceType === "drop") {
|
command += await buildRemoteDocker(application);
|
||||||
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);
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||||
await updateApplicationStatus(applicationId, "done");
|
await updateApplicationStatus(applicationId, "done");
|
||||||
|
|
||||||
@@ -239,6 +227,12 @@ export const deployApplication = async ({
|
|||||||
domains: application.domains,
|
domains: application.domains,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} 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 updateDeploymentStatus(deployment.deploymentId, "error");
|
||||||
await updateApplicationStatus(applicationId, "error");
|
await updateApplicationStatus(applicationId, "error");
|
||||||
|
|
||||||
@@ -253,8 +247,19 @@ export const deployApplication = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
throw error;
|
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;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -276,129 +281,21 @@ export const rebuildApplication = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (application.sourceType === "github") {
|
let command = "set -e;";
|
||||||
await buildApplication(application, deployment.logPath);
|
// Check case for docker only
|
||||||
} else if (application.sourceType === "gitlab") {
|
command += getBuildCommand(application);
|
||||||
await buildApplication(application, deployment.logPath);
|
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
|
||||||
} 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 {
|
|
||||||
if (application.serverId) {
|
if (application.serverId) {
|
||||||
let command = "set -e;";
|
await execAsyncRemote(application.serverId, commandWithLog);
|
||||||
if (application.sourceType === "github") {
|
} else {
|
||||||
command += await getGithubCloneCommand({
|
await execAsync(commandWithLog);
|
||||||
...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 mechanizeDockerContainer(application);
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||||
await updateApplicationStatus(applicationId, "done");
|
await updateApplicationStatus(applicationId, "done");
|
||||||
|
|
||||||
if (application.rollbackActive) {
|
|
||||||
const tagImage =
|
|
||||||
application.sourceType === "docker"
|
|
||||||
? application.dockerImage
|
|
||||||
: application.appName;
|
|
||||||
await createRollback({
|
|
||||||
appName: tagImage || "",
|
|
||||||
deploymentId: deployment.deploymentId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await sendBuildSuccessNotifications({
|
|
||||||
projectName: application.environment.project.name,
|
|
||||||
applicationName: application.name,
|
|
||||||
applicationType: "application",
|
|
||||||
buildLink,
|
|
||||||
organizationId: application.environment.project.organizationId,
|
|
||||||
domains: application.domains,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} 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 updateDeploymentStatus(deployment.deploymentId, "error");
|
||||||
await updateApplicationStatus(applicationId, "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;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -475,14 +372,22 @@ export const deployPreviewApplication = async ({
|
|||||||
application.buildArgs = `${application.previewBuildArgs}\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}`;
|
application.buildSecrets = `${application.previewBuildSecrets}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
||||||
|
|
||||||
|
let command = "set -e;";
|
||||||
if (application.sourceType === "github") {
|
if (application.sourceType === "github") {
|
||||||
await cloneGithubRepository({
|
command += await cloneGithubRepository({
|
||||||
...application,
|
...application,
|
||||||
appName: previewDeployment.appName,
|
appName: previewDeployment.appName,
|
||||||
branch: previewDeployment.branch,
|
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(
|
const successComment = getIssueComment(
|
||||||
application.name,
|
application.name,
|
||||||
@@ -513,170 +418,10 @@ export const deployPreviewApplication = async ({
|
|||||||
return true;
|
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) => {
|
export const getApplicationStats = async (appName: string) => {
|
||||||
|
if (appName === "dokploy") {
|
||||||
|
return await getAdvancedStats(appName);
|
||||||
|
}
|
||||||
const filter = {
|
const filter = {
|
||||||
status: ["running"],
|
status: ["running"],
|
||||||
label: [`com.docker.swarm.service.name=${appName}`],
|
label: [`com.docker.swarm.service.name=${appName}`],
|
||||||
|
|||||||
@@ -616,6 +616,7 @@ const ARVANCLOUD_IP_RANGES = [
|
|||||||
"37.32.18.0/27",
|
"37.32.18.0/27",
|
||||||
"37.32.19.0/27",
|
"37.32.19.0/27",
|
||||||
"185.215.232.0/22",
|
"185.215.232.0/22",
|
||||||
|
"178.131.120.48/28",
|
||||||
];
|
];
|
||||||
|
|
||||||
const CDN_PROVIDERS: CDNProvider[] = [
|
const CDN_PROVIDERS: CDNProvider[] = [
|
||||||
|
|||||||
@@ -7,14 +7,10 @@ import {
|
|||||||
cleanAppName,
|
cleanAppName,
|
||||||
compose,
|
compose,
|
||||||
} from "@dokploy/server/db/schema";
|
} from "@dokploy/server/db/schema";
|
||||||
import {
|
import { getBuildComposeCommand } from "@dokploy/server/utils/builders/compose";
|
||||||
buildCompose,
|
|
||||||
getBuildComposeCommand,
|
|
||||||
} from "@dokploy/server/utils/builders/compose";
|
|
||||||
import { randomizeSpecificationFile } from "@dokploy/server/utils/docker/compose";
|
import { randomizeSpecificationFile } from "@dokploy/server/utils/docker/compose";
|
||||||
import {
|
import {
|
||||||
cloneCompose,
|
cloneCompose,
|
||||||
cloneComposeRemote,
|
|
||||||
loadDockerCompose,
|
loadDockerCompose,
|
||||||
loadDockerComposeRemote,
|
loadDockerComposeRemote,
|
||||||
} from "@dokploy/server/utils/docker/domain";
|
} from "@dokploy/server/utils/docker/domain";
|
||||||
@@ -25,35 +21,23 @@ import {
|
|||||||
execAsync,
|
execAsync,
|
||||||
execAsyncRemote,
|
execAsyncRemote,
|
||||||
} from "@dokploy/server/utils/process/execAsync";
|
} from "@dokploy/server/utils/process/execAsync";
|
||||||
import {
|
import { cloneBitbucketRepository } from "@dokploy/server/utils/providers/bitbucket";
|
||||||
cloneBitbucketRepository,
|
|
||||||
getBitbucketCloneCommand,
|
|
||||||
} from "@dokploy/server/utils/providers/bitbucket";
|
|
||||||
import {
|
import {
|
||||||
cloneGitRepository,
|
cloneGitRepository,
|
||||||
getCustomGitCloneCommand,
|
getGitCommitInfo,
|
||||||
} from "@dokploy/server/utils/providers/git";
|
} from "@dokploy/server/utils/providers/git";
|
||||||
import {
|
import { cloneGiteaRepository } from "@dokploy/server/utils/providers/gitea";
|
||||||
cloneGiteaRepository,
|
import { cloneGithubRepository } from "@dokploy/server/utils/providers/github";
|
||||||
getGiteaCloneCommand,
|
import { cloneGitlabRepository } from "@dokploy/server/utils/providers/gitlab";
|
||||||
} from "@dokploy/server/utils/providers/gitea";
|
import { getCreateComposeFileCommand } from "@dokploy/server/utils/providers/raw";
|
||||||
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 { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { encodeBase64 } from "../utils/docker/utils";
|
|
||||||
import { getDokployUrl } from "./admin";
|
import { getDokployUrl } from "./admin";
|
||||||
import { createDeploymentCompose, updateDeploymentStatus } from "./deployment";
|
import {
|
||||||
|
createDeploymentCompose,
|
||||||
|
updateDeployment,
|
||||||
|
updateDeploymentStatus,
|
||||||
|
} from "./deployment";
|
||||||
import { validUniqueServerAppName } from "./project";
|
import { validUniqueServerAppName } from "./project";
|
||||||
|
|
||||||
export type Compose = typeof compose.$inferSelect;
|
export type Compose = typeof compose.$inferSelect;
|
||||||
@@ -163,10 +147,11 @@ export const loadServices = async (
|
|||||||
const compose = await findComposeById(composeId);
|
const compose = await findComposeById(composeId);
|
||||||
|
|
||||||
if (type === "fetch") {
|
if (type === "fetch") {
|
||||||
|
const command = await cloneCompose(compose);
|
||||||
if (compose.serverId) {
|
if (compose.serverId) {
|
||||||
await cloneComposeRemote(compose);
|
await execAsyncRemote(compose.serverId, command);
|
||||||
} else {
|
} else {
|
||||||
await cloneCompose(compose);
|
await execAsync(command);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,24 +220,41 @@ export const deployCompose = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const entity = {
|
||||||
|
...compose,
|
||||||
|
type: "compose" as const,
|
||||||
|
};
|
||||||
|
let command = "set -e;";
|
||||||
if (compose.sourceType === "github") {
|
if (compose.sourceType === "github") {
|
||||||
await cloneGithubRepository({
|
command += await cloneGithubRepository(entity);
|
||||||
...compose,
|
|
||||||
logPath: deployment.logPath,
|
|
||||||
type: "compose",
|
|
||||||
});
|
|
||||||
} else if (compose.sourceType === "gitlab") {
|
} else if (compose.sourceType === "gitlab") {
|
||||||
await cloneGitlabRepository(compose, deployment.logPath, true);
|
command += await cloneGitlabRepository(entity);
|
||||||
} else if (compose.sourceType === "bitbucket") {
|
} else if (compose.sourceType === "bitbucket") {
|
||||||
await cloneBitbucketRepository(compose, deployment.logPath, true);
|
command += await cloneBitbucketRepository(entity);
|
||||||
} else if (compose.sourceType === "git") {
|
} else if (compose.sourceType === "git") {
|
||||||
await cloneGitRepository(compose, deployment.logPath, true);
|
command += await cloneGitRepository(entity);
|
||||||
} else if (compose.sourceType === "gitea") {
|
} else if (compose.sourceType === "gitea") {
|
||||||
await cloneGiteaRepository(compose, deployment.logPath, true);
|
command += await cloneGiteaRepository(entity);
|
||||||
} else if (compose.sourceType === "raw") {
|
} 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 updateDeploymentStatus(deployment.deploymentId, "done");
|
||||||
await updateCompose(composeId, {
|
await updateCompose(composeId, {
|
||||||
composeStatus: "done",
|
composeStatus: "done",
|
||||||
@@ -281,6 +283,19 @@ export const deployCompose = async ({
|
|||||||
organizationId: compose.environment.project.organizationId,
|
organizationId: compose.environment.project.organizationId,
|
||||||
});
|
});
|
||||||
throw error;
|
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 {
|
try {
|
||||||
|
let command = "set -e;";
|
||||||
if (compose.sourceType === "raw") {
|
if (compose.sourceType === "raw") {
|
||||||
await createComposeFile(compose, deployment.logPath);
|
command += getCreateComposeFileCommand(compose);
|
||||||
}
|
}
|
||||||
await buildCompose(compose, deployment.logPath);
|
|
||||||
|
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
let commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
|
||||||
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 {
|
|
||||||
if (compose.serverId) {
|
if (compose.serverId) {
|
||||||
let command = "set -e;";
|
await execAsyncRemote(compose.serverId, commandWithLog);
|
||||||
|
} else {
|
||||||
if (compose.sourceType === "github") {
|
await execAsync(commandWithLog);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
command += await getBuildComposeCommand(compose);
|
||||||
|
commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
|
||||||
if (compose.serverId) {
|
if (compose.serverId) {
|
||||||
await getBuildComposeCommand(compose, deployment.logPath);
|
await execAsyncRemote(compose.serverId, commandWithLog);
|
||||||
|
} else {
|
||||||
|
await execAsync(commandWithLog);
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||||
@@ -457,16 +341,6 @@ export const rebuildRemoteCompose = async ({
|
|||||||
composeStatus: "done",
|
composeStatus: "done",
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} 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 updateDeploymentStatus(deployment.deploymentId, "error");
|
||||||
await updateCompose(composeId, {
|
await updateCompose(composeId, {
|
||||||
composeStatus: "error",
|
composeStatus: "error",
|
||||||
|
|||||||
@@ -74,20 +74,21 @@ export const createDeployment = async (
|
|||||||
>,
|
>,
|
||||||
) => {
|
) => {
|
||||||
const application = await findApplicationById(deployment.applicationId);
|
const application = await findApplicationById(deployment.applicationId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await removeLastTenDeployments(
|
await removeLastTenDeployments(
|
||||||
deployment.applicationId,
|
deployment.applicationId,
|
||||||
"application",
|
"application",
|
||||||
application.serverId,
|
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 formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
||||||
const fileName = `${application.appName}-${formattedDateTime}.log`;
|
const fileName = `${application.appName}-${formattedDateTime}.log`;
|
||||||
const logFilePath = path.join(LOGS_PATH, application.appName, fileName);
|
const logFilePath = path.join(LOGS_PATH, application.appName, fileName);
|
||||||
|
|
||||||
if (application.serverId) {
|
if (serverId) {
|
||||||
const server = await findServerById(application.serverId);
|
const server = await findServerById(serverId);
|
||||||
|
|
||||||
const command = `
|
const command = `
|
||||||
mkdir -p ${LOGS_PATH}/${application.appName};
|
mkdir -p ${LOGS_PATH}/${application.appName};
|
||||||
@@ -99,7 +100,7 @@ export const createDeployment = async (
|
|||||||
await fsPromises.mkdir(path.join(LOGS_PATH, application.appName), {
|
await fsPromises.mkdir(path.join(LOGS_PATH, application.appName), {
|
||||||
recursive: true,
|
recursive: true,
|
||||||
});
|
});
|
||||||
await fsPromises.writeFile(logFilePath, "Initializing deployment");
|
await fsPromises.writeFile(logFilePath, "Initializing deployment\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
const deploymentCreate = await db
|
const deploymentCreate = await db
|
||||||
@@ -249,7 +250,7 @@ export const createDeploymentCompose = async (
|
|||||||
|
|
||||||
const command = `
|
const command = `
|
||||||
mkdir -p ${LOGS_PATH}/${compose.appName};
|
mkdir -p ${LOGS_PATH}/${compose.appName};
|
||||||
echo "Initializing deployment" >> ${logFilePath};
|
echo "Initializing deployment\n" >> ${logFilePath};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
await execAsyncRemote(server.serverId, command);
|
await execAsyncRemote(server.serverId, command);
|
||||||
@@ -257,7 +258,7 @@ echo "Initializing deployment" >> ${logFilePath};
|
|||||||
await fsPromises.mkdir(path.join(LOGS_PATH, compose.appName), {
|
await fsPromises.mkdir(path.join(LOGS_PATH, compose.appName), {
|
||||||
recursive: true,
|
recursive: true,
|
||||||
});
|
});
|
||||||
await fsPromises.writeFile(logFilePath, "Initializing deployment");
|
await fsPromises.writeFile(logFilePath, "Initializing deployment\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
const deploymentCreate = await db
|
const deploymentCreate = await db
|
||||||
|
|||||||
@@ -34,13 +34,43 @@ export const findEnvironmentById = async (environmentId: string) => {
|
|||||||
const environment = await db.query.environments.findFirst({
|
const environment = await db.query.environments.findFirst({
|
||||||
where: eq(environments.environmentId, environmentId),
|
where: eq(environments.environmentId, environmentId),
|
||||||
with: {
|
with: {
|
||||||
applications: true,
|
applications: {
|
||||||
mariadb: true,
|
with: {
|
||||||
mongo: true,
|
deployments: true,
|
||||||
mysql: true,
|
server: true,
|
||||||
postgres: true,
|
},
|
||||||
redis: true,
|
},
|
||||||
compose: true,
|
mariadb: {
|
||||||
|
with: {
|
||||||
|
server: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mongo: {
|
||||||
|
with: {
|
||||||
|
server: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mysql: {
|
||||||
|
with: {
|
||||||
|
server: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
postgres: {
|
||||||
|
with: {
|
||||||
|
server: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
redis: {
|
||||||
|
with: {
|
||||||
|
server: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
compose: {
|
||||||
|
with: {
|
||||||
|
deployments: true,
|
||||||
|
server: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
project: true,
|
project: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -59,7 +59,8 @@ export const getUpdateData = async (): Promise<IUpdateData> => {
|
|||||||
let currentDigest: string;
|
let currentDigest: string;
|
||||||
try {
|
try {
|
||||||
currentDigest = await getServiceImageDigest();
|
currentDigest = await getServiceImageDigest();
|
||||||
} catch {
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
// Docker service might not exist locally
|
// Docker service might not exist locally
|
||||||
// You can run the # Installation command for docker service create mentioned in the below docs to test it locally:
|
// You can run the # Installation command for docker service create mentioned in the below docs to test it locally:
|
||||||
// https://docs.dokploy.com/docs/core/manual-installation
|
// https://docs.dokploy.com/docs/core/manual-installation
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
import { apikey, member, users_temp } from "@dokploy/server/db/schema";
|
import { apikey, member, user } from "@dokploy/server/db/schema";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import { auth } from "../lib/auth";
|
import { auth } from "../lib/auth";
|
||||||
|
|
||||||
export type User = typeof users_temp.$inferSelect;
|
export type User = typeof user.$inferSelect;
|
||||||
|
|
||||||
export const addNewProject = async (
|
export const addNewProject = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
@@ -403,16 +403,16 @@ export const updateUser = async (userId: string, userData: Partial<User>) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await db
|
const userResult = await db
|
||||||
.update(users_temp)
|
.update(user)
|
||||||
.set({
|
.set({
|
||||||
...userData,
|
...userData,
|
||||||
})
|
})
|
||||||
.where(eq(users_temp.id, userId))
|
.where(eq(user.id, userId))
|
||||||
.returning()
|
.returning()
|
||||||
.then((res) => res[0]);
|
.then((res) => res[0]);
|
||||||
|
|
||||||
return user;
|
return userResult;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createApiKey = async (
|
export const createApiKey = async (
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const TRAEFIK_PORT =
|
|||||||
Number.parseInt(process.env.TRAEFIK_PORT!, 10) || 80;
|
Number.parseInt(process.env.TRAEFIK_PORT!, 10) || 80;
|
||||||
export const TRAEFIK_HTTP3_PORT =
|
export const TRAEFIK_HTTP3_PORT =
|
||||||
Number.parseInt(process.env.TRAEFIK_HTTP3_PORT!, 10) || 443;
|
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 {
|
export interface TraefikOptions {
|
||||||
env?: string[];
|
env?: string[];
|
||||||
|
|||||||
@@ -37,7 +37,16 @@ export const generateRandomDomain = ({
|
|||||||
const hash = randomBytes(3).toString("hex");
|
const hash = randomBytes(3).toString("hex");
|
||||||
const slugIp = serverIp.replaceAll(".", "-").replaceAll(":", "-");
|
const slugIp = serverIp.replaceAll(".", "-").replaceAll(":", "-");
|
||||||
|
|
||||||
return `${projectName}-${hash}${slugIp === "" ? "" : `-${slugIp}`}.traefik.me`;
|
// Domain labels have a max length of 63 characters
|
||||||
|
// Reserve space for: hash (6) + separators (1-2) + ip section + dot + traefik.me (10)
|
||||||
|
// Approx: 6 + 2 + (variable ip length) + 11 = ~19-30 chars for other parts
|
||||||
|
const maxProjectNameLength = 40;
|
||||||
|
const truncatedProjectName =
|
||||||
|
projectName.length > maxProjectNameLength
|
||||||
|
? projectName.substring(0, maxProjectNameLength)
|
||||||
|
: projectName;
|
||||||
|
|
||||||
|
return `${truncatedProjectName}-${hash}${slugIp === "" ? "" : `-${slugIp}`}.traefik.me`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const generateHash = (length = 8): string => {
|
export const generateHash = (length = 8): string => {
|
||||||
|
|||||||
@@ -1,117 +1,28 @@
|
|||||||
import {
|
|
||||||
createWriteStream,
|
|
||||||
existsSync,
|
|
||||||
mkdirSync,
|
|
||||||
writeFileSync,
|
|
||||||
} from "node:fs";
|
|
||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
import { paths } from "@dokploy/server/constants";
|
import { paths } from "@dokploy/server/constants";
|
||||||
import type { InferResultType } from "@dokploy/server/types/with";
|
import type { InferResultType } from "@dokploy/server/types/with";
|
||||||
import boxen from "boxen";
|
import boxen from "boxen";
|
||||||
import {
|
import { writeDomainsToComposeRemote } from "../docker/domain";
|
||||||
writeDomainsToCompose,
|
|
||||||
writeDomainsToComposeRemote,
|
|
||||||
} from "../docker/domain";
|
|
||||||
import {
|
import {
|
||||||
encodeBase64,
|
encodeBase64,
|
||||||
getEnviromentVariablesObject,
|
getEnviromentVariablesObject,
|
||||||
prepareEnvironmentVariables,
|
prepareEnvironmentVariables,
|
||||||
} from "../docker/utils";
|
} from "../docker/utils";
|
||||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
|
||||||
import { spawnAsync } from "../process/spawnAsync";
|
|
||||||
|
|
||||||
export type ComposeNested = InferResultType<
|
export type ComposeNested = InferResultType<
|
||||||
"compose",
|
"compose",
|
||||||
{ environment: { with: { project: true } }; mounts: true; domains: true }
|
{ 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) {
|
export const getBuildComposeCommand = async (compose: ComposeNested) => {
|
||||||
await execAsync(
|
const { COMPOSE_PATH } = paths(!!compose.serverId);
|
||||||
`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);
|
|
||||||
const { sourceType, appName, mounts, composeType, domains } = compose;
|
const { sourceType, appName, mounts, composeType, domains } = compose;
|
||||||
const command = createCommand(compose);
|
const command = createCommand(compose);
|
||||||
const envCommand = getCreateEnvFileCommand(compose);
|
const envCommand = getCreateEnvFileCommand(compose);
|
||||||
const projectPath = join(COMPOSE_PATH, compose.appName, "code");
|
const projectPath = join(COMPOSE_PATH, compose.appName, "code");
|
||||||
const exportEnvCommand = getExportEnvCommand(compose);
|
const exportEnvCommand = getExportEnvCommand(compose);
|
||||||
|
|
||||||
const newCompose = await writeDomainsToComposeRemote(
|
const newCompose = await writeDomainsToComposeRemote(compose, domains);
|
||||||
compose,
|
|
||||||
domains,
|
|
||||||
logPath,
|
|
||||||
);
|
|
||||||
const logContent = `
|
const logContent = `
|
||||||
App Name: ${appName}
|
App Name: ${appName}
|
||||||
Build Compose 🐳
|
Build Compose 🐳
|
||||||
@@ -133,7 +44,7 @@ Compose Type: ${composeType} ✅`;
|
|||||||
const bashCommand = `
|
const bashCommand = `
|
||||||
set -e
|
set -e
|
||||||
{
|
{
|
||||||
echo "${logBox}" >> "${logPath}"
|
echo "${logBox}";
|
||||||
|
|
||||||
${newCompose}
|
${newCompose}
|
||||||
|
|
||||||
@@ -143,17 +54,18 @@ Compose Type: ${composeType} ✅`;
|
|||||||
|
|
||||||
${exportEnvCommand}
|
${exportEnvCommand}
|
||||||
${compose.isolatedDeployment ? `docker network inspect ${compose.appName} >/dev/null 2>&1 || docker network create --attachable ${compose.appName}` : ""}
|
${compose.isolatedDeployment ? `docker network inspect ${compose.appName} >/dev/null 2>&1 || docker network create --attachable ${compose.appName}` : ""}
|
||||||
docker ${command.split(" ").join(" ")} >> "${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` : ""}
|
${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
|
exit 1
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return await execAsyncRemote(compose.serverId, bashCommand);
|
return bashCommand;
|
||||||
|
// return await execAsyncRemote(compose.serverId, bashCommand);
|
||||||
};
|
};
|
||||||
|
|
||||||
const sanitizeCommand = (command: string) => {
|
const sanitizeCommand = (command: string) => {
|
||||||
@@ -185,38 +97,8 @@ export const createCommand = (compose: ComposeNested) => {
|
|||||||
return command;
|
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) => {
|
export const getCreateEnvFileCommand = (compose: ComposeNested) => {
|
||||||
const { COMPOSE_PATH } = paths(true);
|
const { COMPOSE_PATH } = paths(!!compose.serverId);
|
||||||
const { env, composePath, appName } = compose;
|
const { env, composePath, appName } = compose;
|
||||||
const composeFilePath =
|
const composeFilePath =
|
||||||
join(COMPOSE_PATH, appName, "code", composePath) ||
|
join(COMPOSE_PATH, appName, "code", composePath) ||
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { WriteStream } from "node:fs";
|
|
||||||
import {
|
import {
|
||||||
getEnviromentVariablesObject,
|
getEnviromentVariablesObject,
|
||||||
prepareEnvironmentVariables,
|
prepareEnvironmentVariables,
|
||||||
@@ -7,103 +6,10 @@ import {
|
|||||||
getBuildAppDirectory,
|
getBuildAppDirectory,
|
||||||
getDockerContextPath,
|
getDockerContextPath,
|
||||||
} from "../filesystem/directory";
|
} from "../filesystem/directory";
|
||||||
import { spawnAsync } from "../process/spawnAsync";
|
|
||||||
import type { ApplicationNested } from ".";
|
import type { ApplicationNested } from ".";
|
||||||
import { createEnvFile, createEnvFileCommand } from "./utils";
|
import { createEnvFileCommand } from "./utils";
|
||||||
|
|
||||||
export const buildCustomDocker = async (
|
export const getDockerCommand = (application: ApplicationNested) => {
|
||||||
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,
|
|
||||||
) => {
|
|
||||||
const {
|
const {
|
||||||
appName,
|
appName,
|
||||||
env,
|
env,
|
||||||
@@ -176,17 +82,17 @@ export const getDockerCommand = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
command += `
|
command += `
|
||||||
echo "Building ${appName}" >> ${logPath};
|
echo "Building ${appName}" ;
|
||||||
cd ${dockerContextPath} >> ${logPath} 2>> ${logPath} || {
|
cd ${dockerContextPath} || {
|
||||||
echo "❌ The path ${dockerContextPath} does not exist" >> ${logPath};
|
echo "❌ The path ${dockerContextPath} does not exist" ;
|
||||||
exit 1;
|
exit 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
${joinedSecrets} docker ${commandArgs.join(" ")} >> ${logPath} 2>> ${logPath} || {
|
${joinedSecrets} docker ${commandArgs.join(" ")} || {
|
||||||
echo "❌ Docker build failed" >> ${logPath};
|
echo "❌ Docker build failed" ;
|
||||||
exit 1;
|
exit 1;
|
||||||
}
|
}
|
||||||
echo "✅ Docker build completed." >> ${logPath};
|
echo "✅ Docker build completed." ;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return command;
|
return command;
|
||||||
|
|||||||
@@ -1,54 +1,8 @@
|
|||||||
import type { WriteStream } from "node:fs";
|
|
||||||
import { prepareEnvironmentVariables } from "../docker/utils";
|
import { prepareEnvironmentVariables } from "../docker/utils";
|
||||||
import { getBuildAppDirectory } from "../filesystem/directory";
|
import { getBuildAppDirectory } from "../filesystem/directory";
|
||||||
import { spawnAsync } from "../process/spawnAsync";
|
|
||||||
import type { ApplicationNested } from ".";
|
import type { ApplicationNested } from ".";
|
||||||
|
|
||||||
// TODO: integrate in the vps sudo chown -R $(whoami) ~/.docker
|
export const getHerokuCommand = (application: ApplicationNested) => {
|
||||||
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,
|
|
||||||
) => {
|
|
||||||
const { env, appName, cleanCache } = application;
|
const { env, appName, cleanCache } = application;
|
||||||
|
|
||||||
const buildAppDirectory = getBuildAppDirectory(application);
|
const buildAppDirectory = getBuildAppDirectory(application);
|
||||||
@@ -77,12 +31,12 @@ export const getHerokuCommand = (
|
|||||||
|
|
||||||
const command = `pack ${args.join(" ")}`;
|
const command = `pack ${args.join(" ")}`;
|
||||||
const bashCommand = `
|
const bashCommand = `
|
||||||
echo "Starting heroku build..." >> ${logPath};
|
echo "Starting heroku build..." ;
|
||||||
${command} >> ${logPath} 2>> ${logPath} || {
|
${command} || {
|
||||||
echo "❌ Heroku build failed" >> ${logPath};
|
echo "❌ Heroku build failed" ;
|
||||||
exit 1;
|
exit 1;
|
||||||
}
|
}
|
||||||
echo "✅ Heroku build completed." >> ${logPath};
|
echo "✅ Heroku build completed." ;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return bashCommand;
|
return bashCommand;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { createWriteStream } from "node:fs";
|
|
||||||
import type { InferResultType } from "@dokploy/server/types/with";
|
import type { InferResultType } from "@dokploy/server/types/with";
|
||||||
import type { CreateServiceOptions } from "dockerode";
|
import type { CreateServiceOptions } from "dockerode";
|
||||||
import { uploadImage, uploadImageRemoteCommand } from "../cluster/upload";
|
import { uploadImageRemoteCommand } from "../cluster/upload";
|
||||||
import {
|
import {
|
||||||
calculateResources,
|
calculateResources,
|
||||||
generateBindMounts,
|
generateBindMounts,
|
||||||
@@ -11,12 +10,12 @@ import {
|
|||||||
prepareEnvironmentVariables,
|
prepareEnvironmentVariables,
|
||||||
} from "../docker/utils";
|
} from "../docker/utils";
|
||||||
import { getRemoteDocker } from "../servers/remote-docker";
|
import { getRemoteDocker } from "../servers/remote-docker";
|
||||||
import { buildCustomDocker, getDockerCommand } from "./docker-file";
|
import { getDockerCommand } from "./docker-file";
|
||||||
import { buildHeroku, getHerokuCommand } from "./heroku";
|
import { getHerokuCommand } from "./heroku";
|
||||||
import { buildNixpacks, getNixpacksCommand } from "./nixpacks";
|
import { getNixpacksCommand } from "./nixpacks";
|
||||||
import { buildPaketo, getPaketoCommand } from "./paketo";
|
import { getPaketoCommand } from "./paketo";
|
||||||
import { buildRailpack, getRailpackCommand } from "./railpack";
|
import { getRailpackCommand } from "./railpack";
|
||||||
import { buildStatic, getStaticCommand } from "./static";
|
import { getStaticCommand } from "./static";
|
||||||
|
|
||||||
// NIXPACKS codeDirectory = where is the path of the code directory
|
// NIXPACKS codeDirectory = where is the path of the code directory
|
||||||
// HEROKU 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 (
|
export const getBuildCommand = (application: ApplicationNested) => {
|
||||||
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,
|
|
||||||
) => {
|
|
||||||
let command = "";
|
let command = "";
|
||||||
const { buildType, registry } = application;
|
const { buildType, registry } = application;
|
||||||
|
|
||||||
|
if (application.sourceType === "docker") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
switch (buildType) {
|
switch (buildType) {
|
||||||
case "nixpacks":
|
case "nixpacks":
|
||||||
command = getNixpacksCommand(application, logPath);
|
command = getNixpacksCommand(application);
|
||||||
break;
|
break;
|
||||||
case "heroku_buildpacks":
|
case "heroku_buildpacks":
|
||||||
command = getHerokuCommand(application, logPath);
|
command = getHerokuCommand(application);
|
||||||
break;
|
break;
|
||||||
case "paketo_buildpacks":
|
case "paketo_buildpacks":
|
||||||
command = getPaketoCommand(application, logPath);
|
command = getPaketoCommand(application);
|
||||||
break;
|
break;
|
||||||
case "static":
|
case "static":
|
||||||
command = getStaticCommand(application, logPath);
|
command = getStaticCommand(application);
|
||||||
break;
|
break;
|
||||||
case "dockerfile":
|
case "dockerfile":
|
||||||
command = getDockerCommand(application, logPath);
|
command = getDockerCommand(application);
|
||||||
break;
|
break;
|
||||||
case "railpack":
|
case "railpack":
|
||||||
command = getRailpackCommand(application, logPath);
|
command = getRailpackCommand(application);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (registry) {
|
if (registry) {
|
||||||
command += uploadImageRemoteCommand(application, logPath);
|
command += uploadImageRemoteCommand(application);
|
||||||
}
|
}
|
||||||
|
|
||||||
return command;
|
return command;
|
||||||
@@ -143,6 +101,7 @@ export const mechanizeDockerContainer = async (
|
|||||||
UpdateConfig,
|
UpdateConfig,
|
||||||
Networks,
|
Networks,
|
||||||
StopGracePeriod,
|
StopGracePeriod,
|
||||||
|
EndpointSpec,
|
||||||
} = generateConfigContainer(application);
|
} = generateConfigContainer(application);
|
||||||
|
|
||||||
const bindsMount = generateBindMounts(mounts);
|
const bindsMount = generateBindMounts(mounts);
|
||||||
@@ -183,14 +142,16 @@ export const mechanizeDockerContainer = async (
|
|||||||
},
|
},
|
||||||
Mode,
|
Mode,
|
||||||
RollbackConfig,
|
RollbackConfig,
|
||||||
EndpointSpec: {
|
EndpointSpec: EndpointSpec
|
||||||
Ports: ports.map((port) => ({
|
? EndpointSpec
|
||||||
PublishMode: port.publishMode,
|
: {
|
||||||
Protocol: port.protocol,
|
Ports: ports.map((port) => ({
|
||||||
TargetPort: port.targetPort,
|
PublishMode: port.publishMode,
|
||||||
PublishedPort: port.publishedPort,
|
Protocol: port.protocol,
|
||||||
})),
|
TargetPort: port.targetPort,
|
||||||
},
|
PublishedPort: port.publishedPort,
|
||||||
|
})),
|
||||||
|
},
|
||||||
UpdateConfig,
|
UpdateConfig,
|
||||||
...(StopGracePeriod !== undefined &&
|
...(StopGracePeriod !== undefined &&
|
||||||
StopGracePeriod !== null && { StopGracePeriod }),
|
StopGracePeriod !== null && { StopGracePeriod }),
|
||||||
|
|||||||
@@ -1,101 +1,11 @@
|
|||||||
import { existsSync, mkdirSync, type WriteStream } from "node:fs";
|
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import {
|
import { getStaticCommand } from "@dokploy/server/utils/builders/static";
|
||||||
buildStatic,
|
|
||||||
getStaticCommand,
|
|
||||||
} from "@dokploy/server/utils/builders/static";
|
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { prepareEnvironmentVariables } from "../docker/utils";
|
import { prepareEnvironmentVariables } from "../docker/utils";
|
||||||
import { getBuildAppDirectory } from "../filesystem/directory";
|
import { getBuildAppDirectory } from "../filesystem/directory";
|
||||||
import { spawnAsync } from "../process/spawnAsync";
|
|
||||||
import type { ApplicationNested } from ".";
|
import type { ApplicationNested } from ".";
|
||||||
|
|
||||||
export const buildNixpacks = async (
|
export const getNixpacksCommand = (application: ApplicationNested) => {
|
||||||
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,
|
|
||||||
) => {
|
|
||||||
const { env, appName, publishDirectory, cleanCache } = application;
|
const { env, appName, publishDirectory, cleanCache } = application;
|
||||||
|
|
||||||
const buildAppDirectory = getBuildAppDirectory(application);
|
const buildAppDirectory = getBuildAppDirectory(application);
|
||||||
@@ -122,12 +32,12 @@ export const getNixpacksCommand = (
|
|||||||
}
|
}
|
||||||
const command = `nixpacks ${args.join(" ")}`;
|
const command = `nixpacks ${args.join(" ")}`;
|
||||||
let bashCommand = `
|
let bashCommand = `
|
||||||
echo "Starting nixpacks build..." >> ${logPath};
|
echo "Starting nixpacks build..." ;
|
||||||
${command} >> ${logPath} 2>> ${logPath} || {
|
${command} || {
|
||||||
echo "❌ Nixpacks build failed" >> ${logPath};
|
echo "❌ Nixpacks build failed" ;
|
||||||
exit 1;
|
exit 1;
|
||||||
}
|
}
|
||||||
echo "✅ Nixpacks build completed." >> ${logPath};
|
echo "✅ Nixpacks build completed." ;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -141,16 +51,16 @@ echo "✅ Nixpacks build completed." >> ${logPath};
|
|||||||
publishDirectory.endsWith("/") || !path.extname(publishDirectory);
|
publishDirectory.endsWith("/") || !path.extname(publishDirectory);
|
||||||
|
|
||||||
bashCommand += `
|
bashCommand += `
|
||||||
docker create --name ${buildContainerId} ${appName}
|
docker create --name ${buildContainerId} ${appName}
|
||||||
mkdir -p ${localPath}
|
mkdir -p ${localPath}
|
||||||
docker cp ${buildContainerId}:/app/${publishDirectory}${isDirectory ? "/." : ""} ${path.join(buildAppDirectory, publishDirectory)} >> ${logPath} 2>> ${logPath} || {
|
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}
|
docker rm ${buildContainerId}
|
||||||
echo "❌ Copying ${publishDirectory} to ${path.join(buildAppDirectory, publishDirectory)} failed" >> ${logPath};
|
${getStaticCommand(application)}
|
||||||
exit 1;
|
`;
|
||||||
}
|
|
||||||
docker rm ${buildContainerId}
|
|
||||||
${getStaticCommand(application, logPath)}
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return bashCommand;
|
return bashCommand;
|
||||||
|
|||||||
@@ -1,53 +1,8 @@
|
|||||||
import type { WriteStream } from "node:fs";
|
|
||||||
import { prepareEnvironmentVariables } from "../docker/utils";
|
import { prepareEnvironmentVariables } from "../docker/utils";
|
||||||
import { getBuildAppDirectory } from "../filesystem/directory";
|
import { getBuildAppDirectory } from "../filesystem/directory";
|
||||||
import { spawnAsync } from "../process/spawnAsync";
|
|
||||||
import type { ApplicationNested } from ".";
|
import type { ApplicationNested } from ".";
|
||||||
|
|
||||||
export const buildPaketo = async (
|
export const getPaketoCommand = (application: ApplicationNested) => {
|
||||||
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,
|
|
||||||
) => {
|
|
||||||
const { env, appName, cleanCache } = application;
|
const { env, appName, cleanCache } = application;
|
||||||
|
|
||||||
const buildAppDirectory = getBuildAppDirectory(application);
|
const buildAppDirectory = getBuildAppDirectory(application);
|
||||||
@@ -76,12 +31,12 @@ export const getPaketoCommand = (
|
|||||||
|
|
||||||
const command = `pack ${args.join(" ")}`;
|
const command = `pack ${args.join(" ")}`;
|
||||||
const bashCommand = `
|
const bashCommand = `
|
||||||
echo "Starting Paketo build..." >> ${logPath};
|
echo "Starting Paketo build..." ;
|
||||||
${command} >> ${logPath} 2>> ${logPath} || {
|
${command} || {
|
||||||
echo "❌ Paketo build failed" >> ${logPath};
|
echo "❌ Paketo build failed" ;
|
||||||
exit 1;
|
exit 1;
|
||||||
}
|
}
|
||||||
echo "✅ Paketo build completed." >> ${logPath};
|
echo "✅ Paketo build completed." ;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return bashCommand;
|
return bashCommand;
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import { createHash } from "node:crypto";
|
import { createHash } from "node:crypto";
|
||||||
import type { WriteStream } from "node:fs";
|
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import {
|
import {
|
||||||
parseEnvironmentKeyValuePair,
|
parseEnvironmentKeyValuePair,
|
||||||
prepareEnvironmentVariables,
|
prepareEnvironmentVariables,
|
||||||
} from "../docker/utils";
|
} from "../docker/utils";
|
||||||
import { getBuildAppDirectory } from "../filesystem/directory";
|
import { getBuildAppDirectory } from "../filesystem/directory";
|
||||||
import { execAsync } from "../process/execAsync";
|
|
||||||
import { spawnAsync } from "../process/spawnAsync";
|
|
||||||
import type { ApplicationNested } from ".";
|
import type { ApplicationNested } from ".";
|
||||||
|
|
||||||
const calculateSecretsHash = (envVariables: string[]): string => {
|
const calculateSecretsHash = (envVariables: string[]): string => {
|
||||||
@@ -18,108 +15,7 @@ const calculateSecretsHash = (envVariables: string[]): string => {
|
|||||||
return hash.digest("hex");
|
return hash.digest("hex");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const buildRailpack = async (
|
export const getRailpackCommand = (application: ApplicationNested) => {
|
||||||
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,
|
|
||||||
) => {
|
|
||||||
const { env, appName, cleanCache } = application;
|
const { env, appName, cleanCache } = application;
|
||||||
const buildAppDirectory = getBuildAppDirectory(application);
|
const buildAppDirectory = getBuildAppDirectory(application);
|
||||||
const envVariables = prepareEnvironmentVariables(
|
const envVariables = prepareEnvironmentVariables(
|
||||||
@@ -179,25 +75,28 @@ export const getRailpackCommand = (
|
|||||||
buildArgs.push(buildAppDirectory);
|
buildArgs.push(buildAppDirectory);
|
||||||
|
|
||||||
const bashCommand = `
|
const bashCommand = `
|
||||||
|
|
||||||
# Ensure we have a builder with containerd
|
# Ensure we have a builder with containerd
|
||||||
docker buildx create --use --name builder-containerd --driver docker-container || true
|
docker buildx create --use --name builder-containerd --driver docker-container || true
|
||||||
docker buildx use builder-containerd
|
docker buildx use builder-containerd
|
||||||
|
|
||||||
echo "Preparing Railpack build plan..." >> "${logPath}";
|
echo "Preparing Railpack build plan..." ;
|
||||||
railpack ${prepareArgs.join(" ")} >> ${logPath} 2>> ${logPath} || {
|
railpack ${prepareArgs.join(" ")} || {
|
||||||
echo "❌ Railpack prepare failed" >> ${logPath};
|
echo "❌ Railpack prepare failed" ;
|
||||||
|
docker buildx rm builder-containerd || true
|
||||||
exit 1;
|
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
|
# Export environment variables for secrets
|
||||||
${exportEnvs.join("\n")}
|
${exportEnvs.join("\n")}
|
||||||
docker ${buildArgs.join(" ")} >> ${logPath} 2>> ${logPath} || {
|
docker ${buildArgs.join(" ")} || {
|
||||||
echo "❌ Railpack build failed" >> ${logPath};
|
echo "❌ Railpack build failed" ;
|
||||||
|
docker buildx rm builder-containerd || true
|
||||||
exit 1;
|
exit 1;
|
||||||
}
|
}
|
||||||
echo "✅ Railpack build completed." >> ${logPath};
|
echo "✅ Railpack build completed." ;
|
||||||
docker buildx rm builder-containerd
|
docker buildx rm builder-containerd
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import type { WriteStream } from "node:fs";
|
import { getDockerCommand } from "@dokploy/server/utils/builders/docker-file";
|
||||||
import {
|
import { getCreateFileCommand } from "../docker/utils";
|
||||||
buildCustomDocker,
|
|
||||||
getDockerCommand,
|
|
||||||
} from "@dokploy/server/utils/builders/docker-file";
|
|
||||||
import { createFile, getCreateFileCommand } from "../docker/utils";
|
|
||||||
import { getBuildAppDirectory } from "../filesystem/directory";
|
import { getBuildAppDirectory } from "../filesystem/directory";
|
||||||
import type { ApplicationNested } from ".";
|
import type { ApplicationNested } from ".";
|
||||||
|
|
||||||
@@ -32,81 +28,40 @@ http {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const buildStatic = async (
|
export const getStaticCommand = (application: ApplicationNested) => {
|
||||||
application: ApplicationNested,
|
|
||||||
writeStream: WriteStream,
|
|
||||||
) => {
|
|
||||||
const { publishDirectory, isStaticSpa } = application;
|
const { publishDirectory, isStaticSpa } = application;
|
||||||
const buildAppDirectory = getBuildAppDirectory(application);
|
const buildAppDirectory = getBuildAppDirectory(application);
|
||||||
|
let command = "";
|
||||||
try {
|
if (isStaticSpa) {
|
||||||
if (isStaticSpa) {
|
command += getCreateFileCommand(
|
||||||
createFile(buildAppDirectory, "nginx.conf", nginxSpaConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
createFile(
|
|
||||||
buildAppDirectory,
|
buildAppDirectory,
|
||||||
".dockerignore",
|
"nginx.conf",
|
||||||
[".git", ".env", "Dockerfile", ".dockerignore"].join("\n"),
|
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 = (
|
command += getCreateFileCommand(
|
||||||
application: ApplicationNested,
|
buildAppDirectory,
|
||||||
logPath: string,
|
".dockerignore",
|
||||||
) => {
|
[".git", ".env", "Dockerfile", ".dockerignore"].join("\n"),
|
||||||
const { publishDirectory } = application;
|
);
|
||||||
const buildAppDirectory = getBuildAppDirectory(application);
|
|
||||||
|
|
||||||
let command = getCreateFileCommand(
|
command += getCreateFileCommand(
|
||||||
buildAppDirectory,
|
buildAppDirectory,
|
||||||
"Dockerfile",
|
"Dockerfile",
|
||||||
[
|
[
|
||||||
"FROM nginx:alpine",
|
"FROM nginx:alpine",
|
||||||
"WORKDIR /usr/share/nginx/html/",
|
"WORKDIR /usr/share/nginx/html/",
|
||||||
|
isStaticSpa ? "COPY nginx.conf /etc/nginx/nginx.conf" : "",
|
||||||
`COPY ${publishDirectory || "."} .`,
|
`COPY ${publishDirectory || "."} .`,
|
||||||
|
'CMD ["nginx", "-g", "daemon off;"]',
|
||||||
].join("\n"),
|
].join("\n"),
|
||||||
);
|
);
|
||||||
|
|
||||||
command += getDockerCommand(
|
command += getDockerCommand({
|
||||||
{
|
...application,
|
||||||
...application,
|
buildType: "dockerfile",
|
||||||
buildType: "dockerfile",
|
dockerfile: "Dockerfile",
|
||||||
dockerfile: "Dockerfile",
|
});
|
||||||
},
|
|
||||||
logPath,
|
|
||||||
);
|
|
||||||
return command;
|
return command;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
import type { WriteStream } from "node:fs";
|
|
||||||
import type { ApplicationNested } from "../builders";
|
import type { ApplicationNested } from "../builders";
|
||||||
import { spawnAsync } from "../process/spawnAsync";
|
|
||||||
|
|
||||||
export const uploadImage = async (
|
export const uploadImageRemoteCommand = (application: ApplicationNested) => {
|
||||||
application: ApplicationNested,
|
|
||||||
writeStream: WriteStream,
|
|
||||||
) => {
|
|
||||||
const registry = application.registry;
|
const registry = application.registry;
|
||||||
|
|
||||||
if (!registry) {
|
if (!registry) {
|
||||||
@@ -19,85 +14,28 @@ export const uploadImage = async (
|
|||||||
const finalURL = registryUrl;
|
const finalURL = registryUrl;
|
||||||
|
|
||||||
// Build registry tag in correct format: registry.com/owner/image:tag
|
// 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
|
const registryTag = imagePrefix
|
||||||
? `${registryUrl ? `${registryUrl}/` : ""}${imagePrefix}/${imageName}`
|
? `${registryUrl ? `${registryUrl}/` : ""}${imagePrefix}/${imageName}`
|
||||||
: `${registryUrl ? `${registryUrl}/` : ""}${username}/${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 {
|
try {
|
||||||
const command = `
|
const command = `
|
||||||
echo "📦 [Enabled Registry] Uploading image to '${registry.registryType}' | '${registryTag}'" >> ${logPath};
|
echo "📦 [Enabled Registry] Uploading image to '${registry.registryType}' | '${registryTag}'" ;
|
||||||
echo "${registry.password}" | docker login ${finalURL} -u ${registry.username} --password-stdin >> ${logPath} 2>> ${logPath} || {
|
echo "${registry.password}" | docker login ${finalURL} -u ${registry.username} --password-stdin || {
|
||||||
echo "❌ DockerHub Failed" >> ${logPath};
|
echo "❌ DockerHub Failed" ;
|
||||||
exit 1;
|
exit 1;
|
||||||
}
|
}
|
||||||
echo "✅ Registry Login Success" >> ${logPath};
|
echo "✅ Registry Login Success" ;
|
||||||
docker tag ${imageName} ${registryTag} >> ${logPath} 2>> ${logPath} || {
|
docker tag ${imageName} ${registryTag} || {
|
||||||
echo "❌ Error tagging image" >> ${logPath};
|
echo "❌ Error tagging image" ;
|
||||||
exit 1;
|
exit 1;
|
||||||
}
|
}
|
||||||
echo "✅ Image Tagged" >> ${logPath};
|
echo "✅ Image Tagged" ;
|
||||||
docker push ${registryTag} 2>> ${logPath} || {
|
docker push ${registryTag} || {
|
||||||
echo "❌ Error pushing image" >> ${logPath};
|
echo "❌ Error pushing image" ;
|
||||||
exit 1;
|
exit 1;
|
||||||
}
|
}
|
||||||
echo "✅ Image Pushed" >> ${logPath};
|
echo "✅ Image Pushed" ;
|
||||||
`;
|
`;
|
||||||
return command;
|
return command;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
|
|||||||
UpdateConfig,
|
UpdateConfig,
|
||||||
Networks,
|
Networks,
|
||||||
StopGracePeriod,
|
StopGracePeriod,
|
||||||
|
EndpointSpec,
|
||||||
} = generateConfigContainer(mariadb);
|
} = generateConfigContainer(mariadb);
|
||||||
const resources = calculateResources({
|
const resources = calculateResources({
|
||||||
memoryLimit,
|
memoryLimit,
|
||||||
@@ -89,19 +90,21 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
|
|||||||
},
|
},
|
||||||
Mode,
|
Mode,
|
||||||
RollbackConfig,
|
RollbackConfig,
|
||||||
EndpointSpec: {
|
EndpointSpec: EndpointSpec
|
||||||
Mode: "dnsrr",
|
? EndpointSpec
|
||||||
Ports: externalPort
|
: {
|
||||||
? [
|
Mode: "dnsrr" as const,
|
||||||
{
|
Ports: externalPort
|
||||||
Protocol: "tcp",
|
? [
|
||||||
TargetPort: 3306,
|
{
|
||||||
PublishedPort: externalPort,
|
Protocol: "tcp" as const,
|
||||||
PublishMode: "host",
|
TargetPort: 3306,
|
||||||
},
|
PublishedPort: externalPort,
|
||||||
]
|
PublishMode: "host" as const,
|
||||||
: [],
|
},
|
||||||
},
|
]
|
||||||
|
: [],
|
||||||
|
},
|
||||||
UpdateConfig,
|
UpdateConfig,
|
||||||
...(StopGracePeriod !== undefined &&
|
...(StopGracePeriod !== undefined &&
|
||||||
StopGracePeriod !== null && { StopGracePeriod }),
|
StopGracePeriod !== null && { StopGracePeriod }),
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ ${command ?? "wait $MONGOD_PID"}`;
|
|||||||
UpdateConfig,
|
UpdateConfig,
|
||||||
Networks,
|
Networks,
|
||||||
StopGracePeriod,
|
StopGracePeriod,
|
||||||
|
EndpointSpec,
|
||||||
} = generateConfigContainer(mongo);
|
} = generateConfigContainer(mongo);
|
||||||
|
|
||||||
const resources = calculateResources({
|
const resources = calculateResources({
|
||||||
@@ -142,19 +143,21 @@ ${command ?? "wait $MONGOD_PID"}`;
|
|||||||
},
|
},
|
||||||
Mode,
|
Mode,
|
||||||
RollbackConfig,
|
RollbackConfig,
|
||||||
EndpointSpec: {
|
EndpointSpec: EndpointSpec
|
||||||
Mode: "dnsrr",
|
? EndpointSpec
|
||||||
Ports: externalPort
|
: {
|
||||||
? [
|
Mode: "dnsrr" as const,
|
||||||
{
|
Ports: externalPort
|
||||||
Protocol: "tcp",
|
? [
|
||||||
TargetPort: 27017,
|
{
|
||||||
PublishedPort: externalPort,
|
Protocol: "tcp" as const,
|
||||||
PublishMode: "host",
|
TargetPort: 27017,
|
||||||
},
|
PublishedPort: externalPort,
|
||||||
]
|
PublishMode: "host" as const,
|
||||||
: [],
|
},
|
||||||
},
|
]
|
||||||
|
: [],
|
||||||
|
},
|
||||||
UpdateConfig,
|
UpdateConfig,
|
||||||
...(StopGracePeriod !== undefined &&
|
...(StopGracePeriod !== undefined &&
|
||||||
StopGracePeriod !== null && { StopGracePeriod }),
|
StopGracePeriod !== null && { StopGracePeriod }),
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export const buildMysql = async (mysql: MysqlNested) => {
|
|||||||
UpdateConfig,
|
UpdateConfig,
|
||||||
Networks,
|
Networks,
|
||||||
StopGracePeriod,
|
StopGracePeriod,
|
||||||
|
EndpointSpec,
|
||||||
} = generateConfigContainer(mysql);
|
} = generateConfigContainer(mysql);
|
||||||
const resources = calculateResources({
|
const resources = calculateResources({
|
||||||
memoryLimit,
|
memoryLimit,
|
||||||
@@ -95,19 +96,21 @@ export const buildMysql = async (mysql: MysqlNested) => {
|
|||||||
},
|
},
|
||||||
Mode,
|
Mode,
|
||||||
RollbackConfig,
|
RollbackConfig,
|
||||||
EndpointSpec: {
|
EndpointSpec: EndpointSpec
|
||||||
Mode: "dnsrr",
|
? EndpointSpec
|
||||||
Ports: externalPort
|
: {
|
||||||
? [
|
Mode: "dnsrr" as const,
|
||||||
{
|
Ports: externalPort
|
||||||
Protocol: "tcp",
|
? [
|
||||||
TargetPort: 3306,
|
{
|
||||||
PublishedPort: externalPort,
|
Protocol: "tcp" as const,
|
||||||
PublishMode: "host",
|
TargetPort: 3306,
|
||||||
},
|
PublishedPort: externalPort,
|
||||||
]
|
PublishMode: "host" as const,
|
||||||
: [],
|
},
|
||||||
},
|
]
|
||||||
|
: [],
|
||||||
|
},
|
||||||
UpdateConfig,
|
UpdateConfig,
|
||||||
...(StopGracePeriod !== undefined &&
|
...(StopGracePeriod !== undefined &&
|
||||||
StopGracePeriod !== null && { StopGracePeriod }),
|
StopGracePeriod !== null && { StopGracePeriod }),
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export const buildPostgres = async (postgres: PostgresNested) => {
|
|||||||
UpdateConfig,
|
UpdateConfig,
|
||||||
Networks,
|
Networks,
|
||||||
StopGracePeriod,
|
StopGracePeriod,
|
||||||
|
EndpointSpec,
|
||||||
} = generateConfigContainer(postgres);
|
} = generateConfigContainer(postgres);
|
||||||
const resources = calculateResources({
|
const resources = calculateResources({
|
||||||
memoryLimit,
|
memoryLimit,
|
||||||
@@ -88,19 +89,21 @@ export const buildPostgres = async (postgres: PostgresNested) => {
|
|||||||
},
|
},
|
||||||
Mode,
|
Mode,
|
||||||
RollbackConfig,
|
RollbackConfig,
|
||||||
EndpointSpec: {
|
EndpointSpec: EndpointSpec
|
||||||
Mode: "dnsrr",
|
? EndpointSpec
|
||||||
Ports: externalPort
|
: {
|
||||||
? [
|
Mode: "dnsrr" as const,
|
||||||
{
|
Ports: externalPort
|
||||||
Protocol: "tcp",
|
? [
|
||||||
TargetPort: 5432,
|
{
|
||||||
PublishedPort: externalPort,
|
Protocol: "tcp" as const,
|
||||||
PublishMode: "host",
|
TargetPort: 5432,
|
||||||
},
|
PublishedPort: externalPort,
|
||||||
]
|
PublishMode: "host" as const,
|
||||||
: [],
|
},
|
||||||
},
|
]
|
||||||
|
: [],
|
||||||
|
},
|
||||||
UpdateConfig,
|
UpdateConfig,
|
||||||
...(StopGracePeriod !== undefined &&
|
...(StopGracePeriod !== undefined &&
|
||||||
StopGracePeriod !== null && { StopGracePeriod }),
|
StopGracePeriod !== null && { StopGracePeriod }),
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export const buildRedis = async (redis: RedisNested) => {
|
|||||||
UpdateConfig,
|
UpdateConfig,
|
||||||
Networks,
|
Networks,
|
||||||
StopGracePeriod,
|
StopGracePeriod,
|
||||||
|
EndpointSpec,
|
||||||
} = generateConfigContainer(redis);
|
} = generateConfigContainer(redis);
|
||||||
const resources = calculateResources({
|
const resources = calculateResources({
|
||||||
memoryLimit,
|
memoryLimit,
|
||||||
@@ -85,19 +86,21 @@ export const buildRedis = async (redis: RedisNested) => {
|
|||||||
},
|
},
|
||||||
Mode,
|
Mode,
|
||||||
RollbackConfig,
|
RollbackConfig,
|
||||||
EndpointSpec: {
|
EndpointSpec: EndpointSpec
|
||||||
Mode: "dnsrr",
|
? EndpointSpec
|
||||||
Ports: externalPort
|
: {
|
||||||
? [
|
Mode: "dnsrr" as const,
|
||||||
{
|
Ports: externalPort
|
||||||
Protocol: "tcp",
|
? [
|
||||||
TargetPort: 6379,
|
{
|
||||||
PublishedPort: externalPort,
|
Protocol: "tcp" as const,
|
||||||
PublishMode: "host",
|
TargetPort: 6379,
|
||||||
},
|
PublishedPort: externalPort,
|
||||||
]
|
PublishMode: "host" as const,
|
||||||
: [],
|
},
|
||||||
},
|
]
|
||||||
|
: [],
|
||||||
|
},
|
||||||
UpdateConfig,
|
UpdateConfig,
|
||||||
...(StopGracePeriod !== undefined &&
|
...(StopGracePeriod !== undefined &&
|
||||||
StopGracePeriod !== null && { StopGracePeriod }),
|
StopGracePeriod !== null && { StopGracePeriod }),
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { findComposeById } from "@dokploy/server/services/compose";
|
import { findComposeById } from "@dokploy/server/services/compose";
|
||||||
import { stringify } from "yaml";
|
import { stringify } from "yaml";
|
||||||
|
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||||
import { addAppNameToAllServiceNames } from "./collision/root-network";
|
import { addAppNameToAllServiceNames } from "./collision/root-network";
|
||||||
import { generateRandomHash } from "./compose";
|
import { generateRandomHash } from "./compose";
|
||||||
import { addSuffixToAllVolumes } from "./compose/volume";
|
import { addSuffixToAllVolumes } from "./compose/volume";
|
||||||
import {
|
import {
|
||||||
cloneCompose,
|
cloneCompose,
|
||||||
cloneComposeRemote,
|
|
||||||
loadDockerCompose,
|
loadDockerCompose,
|
||||||
loadDockerComposeRemote,
|
loadDockerComposeRemote,
|
||||||
} from "./domain";
|
} from "./domain";
|
||||||
@@ -31,10 +31,11 @@ export const randomizeIsolatedDeploymentComposeFile = async (
|
|||||||
) => {
|
) => {
|
||||||
const compose = await findComposeById(composeId);
|
const compose = await findComposeById(composeId);
|
||||||
|
|
||||||
|
const command = await cloneCompose(compose);
|
||||||
if (compose.serverId) {
|
if (compose.serverId) {
|
||||||
await cloneComposeRemote(compose);
|
await execAsyncRemote(compose.serverId, command);
|
||||||
} else {
|
} else {
|
||||||
await cloneCompose(compose);
|
await execAsync(command);
|
||||||
}
|
}
|
||||||
|
|
||||||
let composeData: ComposeSpecification | null;
|
let composeData: ComposeSpecification | null;
|
||||||
|
|||||||
@@ -1,35 +1,16 @@
|
|||||||
import fs, { existsSync, readFileSync } from "node:fs";
|
import fs, { existsSync, readFileSync } from "node:fs";
|
||||||
import { writeFile } from "node:fs/promises";
|
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { paths } from "@dokploy/server/constants";
|
import { paths } from "@dokploy/server/constants";
|
||||||
import type { Compose } from "@dokploy/server/services/compose";
|
import type { Compose } from "@dokploy/server/services/compose";
|
||||||
import type { Domain } from "@dokploy/server/services/domain";
|
import type { Domain } from "@dokploy/server/services/domain";
|
||||||
import { parse, stringify } from "yaml";
|
import { parse, stringify } from "yaml";
|
||||||
import { execAsyncRemote } from "../process/execAsync";
|
import { execAsyncRemote } from "../process/execAsync";
|
||||||
import {
|
import { cloneBitbucketRepository } from "../providers/bitbucket";
|
||||||
cloneRawBitbucketRepository,
|
import { cloneGitRepository } from "../providers/git";
|
||||||
cloneRawBitbucketRepositoryRemote,
|
import { cloneGiteaRepository } from "../providers/gitea";
|
||||||
} from "../providers/bitbucket";
|
import { cloneGithubRepository } from "../providers/github";
|
||||||
import {
|
import { cloneGitlabRepository } from "../providers/gitlab";
|
||||||
cloneGitRawRepository,
|
import { getCreateComposeFileCommand } from "../providers/raw";
|
||||||
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 { randomizeDeployableSpecificationFile } from "./collision";
|
import { randomizeDeployableSpecificationFile } from "./collision";
|
||||||
import { randomizeSpecificationFile } from "./compose";
|
import { randomizeSpecificationFile } from "./compose";
|
||||||
import type {
|
import type {
|
||||||
@@ -40,35 +21,25 @@ import type {
|
|||||||
import { encodeBase64 } from "./utils";
|
import { encodeBase64 } from "./utils";
|
||||||
|
|
||||||
export const cloneCompose = async (compose: Compose) => {
|
export const cloneCompose = async (compose: Compose) => {
|
||||||
|
let command = "set -e;";
|
||||||
|
const entity = {
|
||||||
|
...compose,
|
||||||
|
type: "compose" as const,
|
||||||
|
};
|
||||||
if (compose.sourceType === "github") {
|
if (compose.sourceType === "github") {
|
||||||
await cloneRawGithubRepository(compose);
|
command += await cloneGithubRepository(entity);
|
||||||
} else if (compose.sourceType === "gitlab") {
|
} else if (compose.sourceType === "gitlab") {
|
||||||
await cloneRawGitlabRepository(compose);
|
command += await cloneGitlabRepository(entity);
|
||||||
} else if (compose.sourceType === "bitbucket") {
|
} else if (compose.sourceType === "bitbucket") {
|
||||||
await cloneRawBitbucketRepository(compose);
|
command += await cloneBitbucketRepository(entity);
|
||||||
} else if (compose.sourceType === "git") {
|
} else if (compose.sourceType === "git") {
|
||||||
await cloneGitRawRepository(compose);
|
command += await cloneGitRepository(entity);
|
||||||
} else if (compose.sourceType === "gitea") {
|
} else if (compose.sourceType === "gitea") {
|
||||||
await cloneRawGiteaRepository(compose);
|
command += await cloneGiteaRepository(entity);
|
||||||
} else if (compose.sourceType === "raw") {
|
} else if (compose.sourceType === "raw") {
|
||||||
await createComposeFileRaw(compose);
|
command += getCreateComposeFileCommand(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);
|
|
||||||
}
|
}
|
||||||
|
return command;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getComposePath = (compose: Compose) => {
|
export const getComposePath = (compose: Compose) => {
|
||||||
@@ -131,28 +102,9 @@ export const readComposeFile = async (compose: Compose) => {
|
|||||||
return null;
|
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 (
|
export const writeDomainsToComposeRemote = async (
|
||||||
compose: Compose,
|
compose: Compose,
|
||||||
domains: Domain[],
|
domains: Domain[],
|
||||||
logPath: string,
|
|
||||||
) => {
|
) => {
|
||||||
if (!domains.length) {
|
if (!domains.length) {
|
||||||
return "";
|
return "";
|
||||||
@@ -164,7 +116,7 @@ export const writeDomainsToComposeRemote = async (
|
|||||||
|
|
||||||
if (!composeConverted) {
|
if (!composeConverted) {
|
||||||
return `
|
return `
|
||||||
echo "❌ Error: Compose file not found" >> ${logPath};
|
echo "❌ Error: Compose file not found";
|
||||||
exit 1;
|
exit 1;
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -175,12 +127,13 @@ exit 1;
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return `echo "❌ Has occured an error: ${error?.message || error}" >> ${logPath};
|
return `echo "❌ Has occured an error: ${error?.message || error}";
|
||||||
exit 1;
|
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 (
|
export const addDomainToCompose = async (
|
||||||
compose: Compose,
|
compose: Compose,
|
||||||
domains: Domain[],
|
domains: Domain[],
|
||||||
@@ -190,7 +143,7 @@ export const addDomainToCompose = async (
|
|||||||
let result: ComposeSpecification | null;
|
let result: ComposeSpecification | null;
|
||||||
|
|
||||||
if (compose.serverId) {
|
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 {
|
} else {
|
||||||
result = await loadDockerCompose(compose);
|
result = await loadDockerCompose(compose);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -395,6 +395,7 @@ export const generateConfigContainer = (
|
|||||||
mounts,
|
mounts,
|
||||||
networkSwarm,
|
networkSwarm,
|
||||||
stopGracePeriodSwarm,
|
stopGracePeriodSwarm,
|
||||||
|
endpointSpecSwarm,
|
||||||
} = application;
|
} = application;
|
||||||
|
|
||||||
const sanitizedStopGracePeriodSwarm =
|
const sanitizedStopGracePeriodSwarm =
|
||||||
@@ -408,11 +409,9 @@ export const generateConfigContainer = (
|
|||||||
...(healthCheckSwarm && {
|
...(healthCheckSwarm && {
|
||||||
HealthCheck: healthCheckSwarm,
|
HealthCheck: healthCheckSwarm,
|
||||||
}),
|
}),
|
||||||
...(restartPolicySwarm
|
...(restartPolicySwarm && {
|
||||||
? {
|
RestartPolicy: restartPolicySwarm,
|
||||||
RestartPolicy: restartPolicySwarm,
|
}),
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
...(placementSwarm
|
...(placementSwarm
|
||||||
? {
|
? {
|
||||||
Placement: placementSwarm,
|
Placement: placementSwarm,
|
||||||
@@ -461,6 +460,18 @@ export const generateConfigContainer = (
|
|||||||
: {
|
: {
|
||||||
Networks: [{ Target: "dokploy-network" }],
|
Networks: [{ Target: "dokploy-network" }],
|
||||||
}),
|
}),
|
||||||
|
...(endpointSpecSwarm && {
|
||||||
|
EndpointSpec: {
|
||||||
|
...(endpointSpecSwarm.Mode && { Mode: endpointSpecSwarm.Mode }),
|
||||||
|
Ports:
|
||||||
|
endpointSpecSwarm.Ports?.map((port) => ({
|
||||||
|
Protocol: (port.Protocol || "tcp") as "tcp" | "udp" | "sctp",
|
||||||
|
TargetPort: port.TargetPort || 0,
|
||||||
|
PublishedPort: port.PublishedPort || 0,
|
||||||
|
PublishMode: (port.PublishMode || "host") as "ingress" | "host",
|
||||||
|
})) || [],
|
||||||
|
},
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -116,11 +116,7 @@ export const execAsyncRemote = async (
|
|||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
resolve({ stdout, stderr });
|
resolve({ stdout, stderr });
|
||||||
} else {
|
} else {
|
||||||
reject(
|
reject(new Error(`Error occurred ❌: ${stderr}`));
|
||||||
new Error(
|
|
||||||
`Command exited with code ${code}. Stderr: ${stderr}, command: ${command}`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on("data", (data: string) => {
|
.on("data", (data: string) => {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { createWriteStream } from "node:fs";
|
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { paths } from "@dokploy/server/constants";
|
import { paths } from "@dokploy/server/constants";
|
||||||
import type {
|
import type {
|
||||||
@@ -9,12 +8,8 @@ import {
|
|||||||
type Bitbucket,
|
type Bitbucket,
|
||||||
findBitbucketById,
|
findBitbucketById,
|
||||||
} from "@dokploy/server/services/bitbucket";
|
} from "@dokploy/server/services/bitbucket";
|
||||||
import type { Compose } from "@dokploy/server/services/compose";
|
|
||||||
import type { InferResultType } from "@dokploy/server/types/with";
|
import type { InferResultType } from "@dokploy/server/types/with";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { recreateDirectory } from "../filesystem/directory";
|
|
||||||
import { execAsyncRemote } from "../process/execAsync";
|
|
||||||
import { spawnAsync } from "../process/spawnAsync";
|
|
||||||
|
|
||||||
export type ApplicationWithBitbucket = InferResultType<
|
export type ApplicationWithBitbucket = InferResultType<
|
||||||
"applications",
|
"applications",
|
||||||
@@ -81,202 +76,52 @@ export const getBitbucketHeaders = (bitbucketProvider: Bitbucket) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cloneBitbucketRepository = async (
|
interface CloneBitbucketRepository {
|
||||||
entity: ApplicationWithBitbucket | ComposeWithBitbucket,
|
appName: string;
|
||||||
logPath: string,
|
bitbucketRepository: string | null;
|
||||||
isCompose = false,
|
bitbucketOwner: string | null;
|
||||||
) => {
|
bitbucketBranch: string | null;
|
||||||
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths();
|
bitbucketId: string | null;
|
||||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
enableSubmodules: boolean;
|
||||||
|
serverId: string | null;
|
||||||
|
type?: "application" | "compose";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cloneBitbucketRepository = async ({
|
||||||
|
type = "application",
|
||||||
|
...entity
|
||||||
|
}: CloneBitbucketRepository) => {
|
||||||
|
let command = "set -e;";
|
||||||
const {
|
const {
|
||||||
appName,
|
appName,
|
||||||
bitbucketRepository,
|
bitbucketRepository,
|
||||||
bitbucketOwner,
|
bitbucketOwner,
|
||||||
bitbucketBranch,
|
bitbucketBranch,
|
||||||
bitbucketId,
|
bitbucketId,
|
||||||
bitbucket,
|
|
||||||
enableSubmodules,
|
enableSubmodules,
|
||||||
|
serverId,
|
||||||
} = entity;
|
} = entity;
|
||||||
|
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(!!serverId);
|
||||||
|
|
||||||
if (!bitbucketId) {
|
if (!bitbucketId) {
|
||||||
throw new TRPCError({
|
command += `echo "Error: ❌ Bitbucket Provider not found"; exit 1;`;
|
||||||
code: "NOT_FOUND",
|
return command;
|
||||||
message: "Bitbucket Provider not found",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
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");
|
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 repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
|
||||||
const cloneUrl = getBitbucketCloneUrl(bitbucket, repoclone);
|
const cloneUrl = getBitbucketCloneUrl(bitbucket, repoclone);
|
||||||
try {
|
command += `echo "Cloning Repo ${repoclone} to ${outputPath}: ✅";`;
|
||||||
writeStream.write(`\nCloning Repo ${repoclone} to ${outputPath}: ✅\n`);
|
command += `git clone --branch ${bitbucketBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} --progress;`;
|
||||||
const cloneArgs = [
|
return command;
|
||||||
"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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getBitbucketRepositories = async (bitbucketId?: string) => {
|
export const getBitbucketRepositories = async (bitbucketId?: string) => {
|
||||||
|
|||||||
@@ -1,60 +1,6 @@
|
|||||||
import { createWriteStream } from "node:fs";
|
import type { ApplicationNested } from "../builders";
|
||||||
import { type ApplicationNested, mechanizeDockerContainer } from "../builders";
|
|
||||||
import { pullImage } from "../docker/utils";
|
|
||||||
|
|
||||||
interface RegistryAuth {
|
export const buildRemoteDocker = async (application: ApplicationNested) => {
|
||||||
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,
|
|
||||||
) => {
|
|
||||||
const { registryUrl, dockerImage, username, password } = application;
|
const { registryUrl, dockerImage, username, password } = application;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -62,25 +8,25 @@ export const buildRemoteDocker = async (
|
|||||||
throw new Error("Docker image not found");
|
throw new Error("Docker image not found");
|
||||||
}
|
}
|
||||||
let command = `
|
let command = `
|
||||||
echo "Pulling ${dockerImage}" >> ${logPath};
|
echo "Pulling ${dockerImage}";
|
||||||
`;
|
`;
|
||||||
|
|
||||||
if (username && password) {
|
if (username && password) {
|
||||||
command += `
|
command += `
|
||||||
if ! echo "${password}" | docker login --username "${username}" --password-stdin "${registryUrl || ""}" >> ${logPath} 2>&1; then
|
if ! echo "${password}" | docker login --username "${username}" --password-stdin "${registryUrl || ""}" 2>&1; then
|
||||||
echo "❌ Login failed" >> ${logPath};
|
echo "❌ Login failed";
|
||||||
exit 1;
|
exit 1;
|
||||||
fi
|
fi
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
command += `
|
command += `
|
||||||
docker pull ${dockerImage} >> ${logPath} 2>> ${logPath} || {
|
docker pull ${dockerImage} 2>&1 || {
|
||||||
echo "❌ Pulling image failed" >> ${logPath};
|
echo "❌ Pulling image failed";
|
||||||
exit 1;
|
exit 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
echo "✅ Pulling image completed." >> ${logPath};
|
echo "✅ Pulling image completed.";
|
||||||
`;
|
`;
|
||||||
return command;
|
return command;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,159 +1,65 @@
|
|||||||
import { createWriteStream } from "node:fs";
|
|
||||||
import path, { join } from "node:path";
|
import path, { join } from "node:path";
|
||||||
import { paths } from "@dokploy/server/constants";
|
import { paths } from "@dokploy/server/constants";
|
||||||
import type { Compose } from "@dokploy/server/services/compose";
|
|
||||||
import {
|
import {
|
||||||
findSSHKeyById,
|
findSSHKeyById,
|
||||||
updateSSHKeyById,
|
updateSSHKeyById,
|
||||||
} from "@dokploy/server/services/ssh-key";
|
} from "@dokploy/server/services/ssh-key";
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import { recreateDirectory } from "../filesystem/directory";
|
|
||||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||||
import { spawnAsync } from "../process/spawnAsync";
|
|
||||||
|
|
||||||
export const cloneGitRepository = async (
|
interface CloneGitRepository {
|
||||||
entity: {
|
appName: string;
|
||||||
appName: string;
|
customGitUrl?: string | null;
|
||||||
customGitUrl?: string | null;
|
customGitBranch?: string | null;
|
||||||
customGitBranch?: string | null;
|
customGitSSHKeyId?: string | null;
|
||||||
customGitSSHKeyId?: string | null;
|
enableSubmodules?: boolean;
|
||||||
enableSubmodules?: boolean;
|
serverId: string | null;
|
||||||
},
|
type?: "application" | "compose";
|
||||||
logPath: string,
|
}
|
||||||
isCompose = false,
|
|
||||||
) => {
|
export const cloneGitRepository = async ({
|
||||||
const { SSH_PATH, COMPOSE_PATH, APPLICATIONS_PATH } = paths();
|
type = "application",
|
||||||
|
...entity
|
||||||
|
}: CloneGitRepository) => {
|
||||||
|
let command = "set -e;";
|
||||||
const {
|
const {
|
||||||
appName,
|
appName,
|
||||||
customGitUrl,
|
customGitUrl,
|
||||||
customGitBranch,
|
customGitBranch,
|
||||||
customGitSSHKeyId,
|
customGitSSHKeyId,
|
||||||
enableSubmodules,
|
enableSubmodules,
|
||||||
|
serverId,
|
||||||
} = entity;
|
} = entity;
|
||||||
|
const { SSH_PATH, COMPOSE_PATH, APPLICATIONS_PATH } = paths(!!serverId);
|
||||||
|
|
||||||
if (!customGitUrl || !customGitBranch) {
|
if (!customGitUrl || !customGitBranch) {
|
||||||
throw new TRPCError({
|
command += `echo "Error: ❌ Repository not found"; exit 1;`;
|
||||||
code: "BAD_REQUEST",
|
return command;
|
||||||
message: "Error: Repository not found",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
|
||||||
const temporalKeyPath = path.join("/tmp", "id_rsa");
|
const temporalKeyPath = path.join("/tmp", "id_rsa");
|
||||||
|
|
||||||
if (customGitSSHKeyId) {
|
if (customGitSSHKeyId) {
|
||||||
const sshKey = await findSSHKeyById(customGitSSHKeyId);
|
const sshKey = await findSSHKeyById(customGitSSHKeyId);
|
||||||
|
|
||||||
await execAsync(`
|
command += `
|
||||||
echo "${sshKey.privateKey}" > ${temporalKeyPath}
|
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 outputPath = join(basePath, appName, "code");
|
||||||
const knownHostsPath = path.join(SSH_PATH, "known_hosts");
|
const knownHostsPath = path.join(SSH_PATH, "known_hosts");
|
||||||
|
|
||||||
try {
|
if (!isHttpOrHttps(customGitUrl)) {
|
||||||
if (!isHttpOrHttps(customGitUrl)) {
|
if (!customGitSSHKeyId) {
|
||||||
if (!customGitSSHKeyId) {
|
command += `echo "Error: ❌ You are trying to clone a ssh repository without a ssh key, please set a ssh key"; exit 1;`;
|
||||||
throw new TRPCError({
|
return command;
|
||||||
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);
|
command += addHostToKnownHostsCommand(customGitUrl);
|
||||||
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 += `rm -rf ${outputPath};`;
|
||||||
|
command += `mkdir -p ${outputPath};`;
|
||||||
export const getCustomGitCloneCommand = async (
|
command += `echo "Cloning Repo Custom ${customGitUrl} to ${outputPath}: ✅";`;
|
||||||
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");
|
|
||||||
|
|
||||||
if (customGitSSHKeyId) {
|
if (customGitSSHKeyId) {
|
||||||
await updateSSHKeyById({
|
await updateSSHKeyById({
|
||||||
@@ -161,48 +67,22 @@ export const getCustomGitCloneCommand = async (
|
|||||||
lastUsedAt: new Date().toISOString(),
|
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 (customGitSSHKeyId) {
|
||||||
`if ! git clone --branch ${customGitBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${customGitUrl} ${outputPath} >> ${logPath} 2>&1; then
|
const sshKey = await findSSHKeyById(customGitSSHKeyId);
|
||||||
echo "❌ [ERROR] Fail to clone the repository ${customGitUrl}" >> ${logPath};
|
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;
|
exit 1;
|
||||||
fi
|
fi
|
||||||
`,
|
`;
|
||||||
);
|
|
||||||
command.push(`echo "Cloned Custom Git ${customGitUrl}: ✅" >> ${logPath};`);
|
return command;
|
||||||
return command.join("\n");
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const isHttpOrHttps = (url: string): boolean => {
|
const isHttpOrHttps = (url: string): boolean => {
|
||||||
@@ -210,19 +90,19 @@ const isHttpOrHttps = (url: string): boolean => {
|
|||||||
return regex.test(url);
|
return regex.test(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
const addHostToKnownHosts = async (repositoryURL: string) => {
|
// const addHostToKnownHosts = async (repositoryURL: string) => {
|
||||||
const { SSH_PATH } = paths();
|
// const { SSH_PATH } = paths();
|
||||||
const { domain, port } = sanitizeRepoPathSSH(repositoryURL);
|
// const { domain, port } = sanitizeRepoPathSSH(repositoryURL);
|
||||||
const knownHostsPath = path.join(SSH_PATH, "known_hosts");
|
// const knownHostsPath = path.join(SSH_PATH, "known_hosts");
|
||||||
|
|
||||||
const command = `ssh-keyscan -p ${port} ${domain} >> ${knownHostsPath}`;
|
// const command = `ssh-keyscan -p ${port} ${domain} >> ${knownHostsPath}`;
|
||||||
try {
|
// try {
|
||||||
await execAsync(command);
|
// await execAsync(command);
|
||||||
} catch (error) {
|
// } catch (error) {
|
||||||
console.error(`Error adding host to known_hosts: ${error}`);
|
// console.error(`Error adding host to known_hosts: ${error}`);
|
||||||
throw error;
|
// throw error;
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|
||||||
const addHostToKnownHostsCommand = (repositoryURL: string) => {
|
const addHostToKnownHostsCommand = (repositoryURL: string) => {
|
||||||
const { SSH_PATH } = paths(true);
|
const { SSH_PATH } = paths(true);
|
||||||
@@ -267,160 +147,43 @@ const sanitizeRepoPathSSH = (input: string) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cloneGitRawRepository = async (entity: {
|
interface Props {
|
||||||
appName: string;
|
appName: string;
|
||||||
customGitUrl?: string | null;
|
type?: "application" | "compose";
|
||||||
customGitBranch?: string | null;
|
serverId: string | null;
|
||||||
customGitSSHKeyId?: string | null;
|
}
|
||||||
enableSubmodules?: boolean;
|
|
||||||
}) => {
|
|
||||||
const {
|
|
||||||
appName,
|
|
||||||
customGitUrl,
|
|
||||||
customGitBranch,
|
|
||||||
customGitSSHKeyId,
|
|
||||||
enableSubmodules,
|
|
||||||
} = entity;
|
|
||||||
|
|
||||||
if (!customGitUrl || !customGitBranch) {
|
export const getGitCommitInfo = async ({
|
||||||
throw new TRPCError({
|
appName,
|
||||||
code: "BAD_REQUEST",
|
type = "application",
|
||||||
message: "Error: Repository not found",
|
serverId,
|
||||||
});
|
}: Props) => {
|
||||||
}
|
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(!!serverId);
|
||||||
|
const basePath = type === "compose" ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||||
const { SSH_PATH, COMPOSE_PATH } = paths();
|
|
||||||
const temporalKeyPath = path.join("/tmp", "id_rsa");
|
|
||||||
const basePath = COMPOSE_PATH;
|
|
||||||
const outputPath = join(basePath, appName, "code");
|
const outputPath = join(basePath, appName, "code");
|
||||||
const knownHostsPath = path.join(SSH_PATH, "known_hosts");
|
let stdoutResult = "";
|
||||||
|
const result = {
|
||||||
if (customGitSSHKeyId) {
|
message: "",
|
||||||
const sshKey = await findSSHKeyById(customGitSSHKeyId);
|
hash: "",
|
||||||
|
};
|
||||||
await execAsync(`
|
|
||||||
echo "${sshKey.privateKey}" > ${temporalKeyPath}
|
|
||||||
chmod 600 ${temporalKeyPath}
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!isHttpOrHttps(customGitUrl)) {
|
const gitCommand = `git -C ${outputPath} log -1 --pretty=format:"%H---DELIMITER---%B"`;
|
||||||
if (!customGitSSHKeyId) {
|
if (serverId) {
|
||||||
throw new TRPCError({
|
const { stdout } = await execAsyncRemote(serverId, gitCommand);
|
||||||
code: "BAD_REQUEST",
|
stdoutResult = stdout.trim();
|
||||||
message:
|
} else {
|
||||||
"Error: you are trying to clone a ssh repository without a ssh key, please set a ssh key",
|
const { stdout } = await execAsync(gitCommand);
|
||||||
});
|
stdoutResult = stdout.trim();
|
||||||
}
|
|
||||||
await addHostToKnownHosts(customGitUrl);
|
|
||||||
}
|
|
||||||
await recreateDirectory(outputPath);
|
|
||||||
|
|
||||||
if (customGitSSHKeyId) {
|
|
||||||
await updateSSHKeyById({
|
|
||||||
sshKeyId: customGitSSHKeyId,
|
|
||||||
lastUsedAt: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { port } = sanitizeRepoPathSSH(customGitUrl);
|
const parts = stdoutResult.split("---DELIMITER---");
|
||||||
const cloneArgs = [
|
if (parts && parts.length === 2) {
|
||||||
"clone",
|
result.hash = parts[0]?.trim() || "";
|
||||||
"--branch",
|
result.message = parts[1]?.trim() || "";
|
||||||
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}`,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
console.error(`Error getting git commit info: ${error}`);
|
||||||
}
|
return null;
|
||||||
};
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
return result;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { createWriteStream } from "node:fs";
|
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { paths } from "@dokploy/server/constants";
|
import { paths } from "@dokploy/server/constants";
|
||||||
import type { Compose } from "@dokploy/server/services/compose";
|
|
||||||
import {
|
import {
|
||||||
findGiteaById,
|
findGiteaById,
|
||||||
type Gitea,
|
type Gitea,
|
||||||
@@ -9,9 +7,6 @@ import {
|
|||||||
} from "@dokploy/server/services/gitea";
|
} from "@dokploy/server/services/gitea";
|
||||||
import type { InferResultType } from "@dokploy/server/types/with";
|
import type { InferResultType } from "@dokploy/server/types/with";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { recreateDirectory } from "../filesystem/directory";
|
|
||||||
import { execAsyncRemote } from "../process/execAsync";
|
|
||||||
import { spawnAsync } from "../process/spawnAsync";
|
|
||||||
|
|
||||||
export const getErrorCloneRequirements = (entity: {
|
export const getErrorCloneRequirements = (entity: {
|
||||||
giteaRepository?: string | null;
|
giteaRepository?: string | null;
|
||||||
@@ -119,79 +114,27 @@ export type ApplicationWithGitea = InferResultType<
|
|||||||
|
|
||||||
export type ComposeWithGitea = InferResultType<"compose", { gitea: true }>;
|
export type ComposeWithGitea = InferResultType<"compose", { gitea: true }>;
|
||||||
|
|
||||||
export const getGiteaCloneCommand = async (
|
type GiteaClone = (ApplicationWithGitea | ComposeWithGitea) & {
|
||||||
entity: ApplicationWithGitea | ComposeWithGitea,
|
serverId: string | null;
|
||||||
logPath: string,
|
type?: "application" | "compose";
|
||||||
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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cloneGiteaRepository = async (
|
interface CloneGiteaRepository {
|
||||||
entity: ApplicationWithGitea | ComposeWithGitea,
|
appName: string;
|
||||||
logPath: string,
|
giteaBranch: string | null;
|
||||||
isCompose = false,
|
giteaId: string | null;
|
||||||
) => {
|
giteaOwner: string | null;
|
||||||
const { APPLICATIONS_PATH, COMPOSE_PATH } = paths();
|
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 {
|
const {
|
||||||
appName,
|
appName,
|
||||||
giteaBranch,
|
giteaBranch,
|
||||||
@@ -199,27 +142,27 @@ export const cloneGiteaRepository = async (
|
|||||||
giteaOwner,
|
giteaOwner,
|
||||||
giteaRepository,
|
giteaRepository,
|
||||||
enableSubmodules,
|
enableSubmodules,
|
||||||
|
serverId,
|
||||||
} = entity;
|
} = entity;
|
||||||
|
const { APPLICATIONS_PATH, COMPOSE_PATH } = paths(!!serverId);
|
||||||
|
|
||||||
if (!giteaId) {
|
if (!giteaId) {
|
||||||
throw new TRPCError({
|
command += `echo "Error: ❌ Gitea Provider not found"; exit 1;`;
|
||||||
code: "NOT_FOUND",
|
return command;
|
||||||
message: "Gitea Provider not found",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await refreshGiteaToken(giteaId);
|
await refreshGiteaToken(giteaId);
|
||||||
const giteaProvider = await findGiteaById(giteaId);
|
const giteaProvider = await findGiteaById(giteaId);
|
||||||
|
|
||||||
if (!giteaProvider) {
|
if (!giteaProvider) {
|
||||||
throw new TRPCError({
|
command += `echo "❌ [ERROR] Gitea provider not found in the database"; exit 1;`;
|
||||||
code: "NOT_FOUND",
|
return command;
|
||||||
message: "Gitea provider not found in the database",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
const basePath = type === "compose" ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||||
const outputPath = join(basePath, appName, "code");
|
const outputPath = join(basePath, appName, "code");
|
||||||
await recreateDirectory(outputPath);
|
command += `rm -rf ${outputPath};`;
|
||||||
|
command += `mkdir -p ${outputPath};`;
|
||||||
|
|
||||||
const repoClone = `${giteaOwner}/${giteaRepository}.git`;
|
const repoClone = `${giteaOwner}/${giteaRepository}.git`;
|
||||||
const cloneUrl = buildGiteaCloneUrl(
|
const cloneUrl = buildGiteaCloneUrl(
|
||||||
@@ -229,134 +172,9 @@ export const cloneGiteaRepository = async (
|
|||||||
giteaRepository!,
|
giteaRepository!,
|
||||||
);
|
);
|
||||||
|
|
||||||
writeStream.write(`\nCloning Repo ${repoClone} to ${outputPath}...\n`);
|
command += `echo "Cloning Repo ${repoClone} to ${outputPath}: ✅";`;
|
||||||
|
command += `git clone --branch ${giteaBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} --progress;`;
|
||||||
try {
|
return command;
|
||||||
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;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const haveGiteaRequirements = (giteaProvider: Gitea) => {
|
export const haveGiteaRequirements = (giteaProvider: Gitea) => {
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
import { createWriteStream } from "node:fs";
|
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { paths } from "@dokploy/server/constants";
|
import { paths } from "@dokploy/server/constants";
|
||||||
import type { apiFindGithubBranches } from "@dokploy/server/db/schema";
|
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 { findGithubById, type Github } from "@dokploy/server/services/github";
|
||||||
import type { InferResultType } from "@dokploy/server/types/with";
|
import type { InferResultType } from "@dokploy/server/types/with";
|
||||||
import { createAppAuth } from "@octokit/auth-app";
|
import { createAppAuth } from "@octokit/auth-app";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { Octokit } from "octokit";
|
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 => {
|
export const authGithub = (githubProvider: Github): Octokit => {
|
||||||
if (!haveGithubRequirements(githubProvider)) {
|
if (!haveGithubRequirements(githubProvider)) {
|
||||||
@@ -123,42 +118,39 @@ interface CloneGithubRepository {
|
|||||||
branch: string | null;
|
branch: string | null;
|
||||||
githubId: string | null;
|
githubId: string | null;
|
||||||
repository: string | null;
|
repository: string | null;
|
||||||
logPath: string;
|
|
||||||
type?: "application" | "compose";
|
type?: "application" | "compose";
|
||||||
enableSubmodules: boolean;
|
enableSubmodules: boolean;
|
||||||
|
serverId: string | null;
|
||||||
}
|
}
|
||||||
export const cloneGithubRepository = async ({
|
export const cloneGithubRepository = async ({
|
||||||
logPath,
|
|
||||||
type = "application",
|
type = "application",
|
||||||
...entity
|
...entity
|
||||||
}: CloneGithubRepository) => {
|
}: CloneGithubRepository) => {
|
||||||
|
let command = "set -e;";
|
||||||
const isCompose = type === "compose";
|
const isCompose = type === "compose";
|
||||||
const { APPLICATIONS_PATH, COMPOSE_PATH } = paths();
|
const {
|
||||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
appName,
|
||||||
const { appName, repository, owner, branch, githubId, enableSubmodules } =
|
repository,
|
||||||
entity;
|
owner,
|
||||||
|
branch,
|
||||||
|
githubId,
|
||||||
|
enableSubmodules,
|
||||||
|
serverId,
|
||||||
|
} = entity;
|
||||||
|
const { APPLICATIONS_PATH, COMPOSE_PATH } = paths(!!serverId);
|
||||||
|
|
||||||
if (!githubId) {
|
if (!githubId) {
|
||||||
throw new TRPCError({
|
command += `echo "Error: ❌ Github Provider not found"; exit 1;`;
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "GitHub Provider not found",
|
return command;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const requirements = getErrorCloneRequirements(entity);
|
const requirements = getErrorCloneRequirements(entity);
|
||||||
|
|
||||||
// Check if requirements are met
|
// Check if requirements are met
|
||||||
if (requirements.length > 0) {
|
if (requirements.length > 0) {
|
||||||
writeStream.write(
|
command += `echo "GitHub Repository configuration failed for application: ${appName}"; echo "Reasons:"; echo "${requirements.join("\n")}"; exit 1;`;
|
||||||
`\nGitHub Repository configuration failed for application: ${appName}\n`,
|
return command;
|
||||||
);
|
|
||||||
writeStream.write("Reasons:\n");
|
|
||||||
writeStream.write(requirements.join("\n"));
|
|
||||||
writeStream.end();
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error: GitHub repository information is incomplete.",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const githubProvider = await findGithubById(githubId);
|
const githubProvider = await findGithubById(githubId);
|
||||||
@@ -167,193 +159,15 @@ export const cloneGithubRepository = async ({
|
|||||||
const octokit = authGithub(githubProvider);
|
const octokit = authGithub(githubProvider);
|
||||||
const token = await getGithubToken(octokit);
|
const token = await getGithubToken(octokit);
|
||||||
const repoclone = `github.com/${owner}/${repository}.git`;
|
const repoclone = `github.com/${owner}/${repository}.git`;
|
||||||
await recreateDirectory(outputPath);
|
// await recreateDirectory(outputPath);
|
||||||
|
command += `rm -rf ${outputPath};`;
|
||||||
|
command += `mkdir -p ${outputPath};`;
|
||||||
const cloneUrl = `https://oauth2:${token}@${repoclone}`;
|
const cloneUrl = `https://oauth2:${token}@${repoclone}`;
|
||||||
|
|
||||||
try {
|
command += `echo "Cloning Repo ${repoclone} to ${outputPath}: ✅";`;
|
||||||
writeStream.write(`\nCloning Repo ${repoclone} to ${outputPath}: ✅\n`);
|
command += `git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} --progress;`;
|
||||||
const cloneArgs = [
|
|
||||||
"clone",
|
|
||||||
"--branch",
|
|
||||||
branch!,
|
|
||||||
"--depth",
|
|
||||||
"1",
|
|
||||||
...(enableSubmodules ? ["--recurse-submodules"] : []),
|
|
||||||
cloneUrl,
|
|
||||||
outputPath,
|
|
||||||
"--progress",
|
|
||||||
];
|
|
||||||
|
|
||||||
await spawnAsync("git", cloneArgs, (data) => {
|
return command;
|
||||||
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;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getGithubRepositories = async (githubId?: string) => {
|
export const getGithubRepositories = async (githubId?: string) => {
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { createWriteStream } from "node:fs";
|
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { paths } from "@dokploy/server/constants";
|
import { paths } from "@dokploy/server/constants";
|
||||||
import type { apiGitlabTestConnection } from "@dokploy/server/db/schema";
|
import type { apiGitlabTestConnection } from "@dokploy/server/db/schema";
|
||||||
import type { Compose } from "@dokploy/server/services/compose";
|
|
||||||
import {
|
import {
|
||||||
findGitlabById,
|
findGitlabById,
|
||||||
type Gitlab,
|
type Gitlab,
|
||||||
@@ -10,9 +8,6 @@ import {
|
|||||||
} from "@dokploy/server/services/gitlab";
|
} from "@dokploy/server/services/gitlab";
|
||||||
import type { InferResultType } from "@dokploy/server/types/with";
|
import type { InferResultType } from "@dokploy/server/types/with";
|
||||||
import { TRPCError } from "@trpc/server";
|
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) => {
|
export const refreshGitlabToken = async (gitlabProviderId: string) => {
|
||||||
const gitlabProvider = await findGitlabById(gitlabProviderId);
|
const gitlabProvider = await findGitlabById(gitlabProviderId);
|
||||||
@@ -102,25 +97,34 @@ const getGitlabCloneUrl = (gitlab: GitlabInfo, repoClone: string) => {
|
|||||||
return cloneUrl;
|
return cloneUrl;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cloneGitlabRepository = async (
|
interface CloneGitlabRepository {
|
||||||
entity: ApplicationWithGitlab | ComposeWithGitlab,
|
appName: string;
|
||||||
logPath: string,
|
gitlabBranch: string | null;
|
||||||
isCompose = false,
|
gitlabId: string | null;
|
||||||
) => {
|
gitlabPathNamespace: string | null;
|
||||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
enableSubmodules: boolean;
|
||||||
|
serverId: string | null;
|
||||||
|
type?: "application" | "compose";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cloneGitlabRepository = async ({
|
||||||
|
type = "application",
|
||||||
|
...entity
|
||||||
|
}: CloneGitlabRepository) => {
|
||||||
|
let command = "set -e;";
|
||||||
const {
|
const {
|
||||||
appName,
|
appName,
|
||||||
gitlabBranch,
|
gitlabBranch,
|
||||||
gitlabId,
|
gitlabId,
|
||||||
gitlabPathNamespace,
|
gitlabPathNamespace,
|
||||||
enableSubmodules,
|
enableSubmodules,
|
||||||
|
serverId,
|
||||||
} = entity;
|
} = entity;
|
||||||
|
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(!!serverId);
|
||||||
|
|
||||||
if (!gitlabId) {
|
if (!gitlabId) {
|
||||||
throw new TRPCError({
|
command += `echo "Error: ❌ Gitlab Provider not found"; exit 1;`;
|
||||||
code: "NOT_FOUND",
|
return command;
|
||||||
message: "Gitlab Provider not found",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await refreshGitlabToken(gitlabId);
|
await refreshGitlabToken(gitlabId);
|
||||||
@@ -130,127 +134,19 @@ export const cloneGitlabRepository = async (
|
|||||||
|
|
||||||
// Check if requirements are met
|
// Check if requirements are met
|
||||||
if (requirements.length > 0) {
|
if (requirements.length > 0) {
|
||||||
writeStream.write(
|
command += `echo "❌ [ERROR] GitLab Repository configuration failed for application: ${appName}"; echo "Reasons:"; echo "${requirements.join("\n")}"; exit 1;`;
|
||||||
`\nGitLab Repository configuration failed for application: ${appName}\n`,
|
return command;
|
||||||
);
|
|
||||||
writeStream.write("Reasons:\n");
|
|
||||||
writeStream.write(requirements.join("\n"));
|
|
||||||
writeStream.end();
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error: GitLab repository information is incomplete.",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths();
|
const basePath = type === "compose" ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
|
||||||
const outputPath = join(basePath, appName, "code");
|
const outputPath = join(basePath, appName, "code");
|
||||||
await recreateDirectory(outputPath);
|
command += `rm -rf ${outputPath};`;
|
||||||
|
command += `mkdir -p ${outputPath};`;
|
||||||
const repoClone = getGitlabRepoClone(gitlab, gitlabPathNamespace);
|
const repoClone = getGitlabRepoClone(gitlab, gitlabPathNamespace);
|
||||||
const cloneUrl = getGitlabCloneUrl(gitlab, repoClone);
|
const cloneUrl = getGitlabCloneUrl(gitlab, repoClone);
|
||||||
try {
|
command += `echo "Cloning Repo ${repoClone} to ${outputPath}: ✅";`;
|
||||||
writeStream.write(`\nCloning Repo ${repoClone} to ${outputPath}: ✅\n`);
|
command += `git clone --branch ${gitlabBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} --progress;`;
|
||||||
const cloneArgs = [
|
return command;
|
||||||
"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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getGitlabRepositories = async (gitlabId?: string) => {
|
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 (
|
export const testGitlabConnection = async (
|
||||||
input: typeof apiGitlabTestConnection._type,
|
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 { join } from "node:path";
|
||||||
import { paths } from "@dokploy/server/constants";
|
import { paths } from "@dokploy/server/constants";
|
||||||
import type { Compose } from "@dokploy/server/services/compose";
|
import type { Compose } from "@dokploy/server/services/compose";
|
||||||
import { encodeBase64 } from "../docker/utils";
|
import { encodeBase64 } from "../docker/utils";
|
||||||
import { recreateDirectory } from "../filesystem/directory";
|
|
||||||
import { execAsyncRemote } from "../process/execAsync";
|
|
||||||
|
|
||||||
export const createComposeFile = async (compose: Compose, logPath: string) => {
|
export const getCreateComposeFileCommand = (compose: Compose) => {
|
||||||
const { COMPOSE_PATH } = paths();
|
const { COMPOSE_PATH } = paths(!!compose.serverId);
|
||||||
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);
|
|
||||||
const { appName, composeFile } = compose;
|
const { appName, composeFile } = compose;
|
||||||
const outputPath = join(COMPOSE_PATH, appName, "code");
|
const outputPath = join(COMPOSE_PATH, appName, "code");
|
||||||
const filePath = join(outputPath, "docker-compose.yml");
|
const filePath = join(outputPath, "docker-compose.yml");
|
||||||
@@ -43,39 +13,7 @@ export const getCreateComposeFileCommand = (
|
|||||||
rm -rf ${outputPath};
|
rm -rf ${outputPath};
|
||||||
mkdir -p ${outputPath};
|
mkdir -p ${outputPath};
|
||||||
echo "${encodedContent}" | base64 -d > "${filePath}";
|
echo "${encodedContent}" | base64 -d > "${filePath}";
|
||||||
echo "File 'docker-compose.yml' created: ✅" >> ${logPath};
|
echo "File 'docker-compose.yml' created: ✅";
|
||||||
`;
|
`;
|
||||||
return bashCommand;
|
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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export const getPostgresRestoreCommand = (
|
|||||||
database: string,
|
database: string,
|
||||||
databaseUser: string,
|
databaseUser: string,
|
||||||
) => {
|
) => {
|
||||||
return `docker exec -i $CONTAINER_ID sh -c "pg_restore -U ${databaseUser} -d ${database} -O --clean --if-exists"`;
|
return `docker exec -i $CONTAINER_ID sh -c "pg_restore -U '${databaseUser}' -d ${database} -O --clean --if-exists"`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMariadbRestoreCommand = (
|
export const getMariadbRestoreCommand = (
|
||||||
@@ -15,14 +15,14 @@ export const getMariadbRestoreCommand = (
|
|||||||
databaseUser: string,
|
databaseUser: string,
|
||||||
databasePassword: string,
|
databasePassword: string,
|
||||||
) => {
|
) => {
|
||||||
return `docker exec -i $CONTAINER_ID sh -c "mariadb -u ${databaseUser} -p${databasePassword} ${database}"`;
|
return `docker exec -i $CONTAINER_ID sh -c "mariadb -u '${databaseUser}' -p'${databasePassword}' ${database}"`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMysqlRestoreCommand = (
|
export const getMysqlRestoreCommand = (
|
||||||
database: string,
|
database: string,
|
||||||
databasePassword: string,
|
databasePassword: string,
|
||||||
) => {
|
) => {
|
||||||
return `docker exec -i $CONTAINER_ID sh -c "mysql -u root -p${databasePassword} ${database}"`;
|
return `docker exec -i $CONTAINER_ID sh -c "mysql -u root -p'${databasePassword}' ${database}"`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMongoRestoreCommand = (
|
export const getMongoRestoreCommand = (
|
||||||
@@ -30,7 +30,7 @@ export const getMongoRestoreCommand = (
|
|||||||
databaseUser: string,
|
databaseUser: string,
|
||||||
databasePassword: string,
|
databasePassword: string,
|
||||||
) => {
|
) => {
|
||||||
return `docker exec -i $CONTAINER_ID sh -c "mongorestore --username ${databaseUser} --password ${databasePassword} --authenticationDatabase admin --db ${database} --archive"`;
|
return `docker exec -i $CONTAINER_ID sh -c "mongorestore --username '${databaseUser}' --password '${databasePassword}' --authenticationDatabase admin --db ${database} --archive"`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getComposeSearchCommand = (
|
export const getComposeSearchCommand = (
|
||||||
|
|||||||
26
pnpm-lock.yaml
generated
26
pnpm-lock.yaml
generated
@@ -347,8 +347,8 @@ importers:
|
|||||||
specifier: ^0.2.1
|
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)
|
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:
|
node-os-utils:
|
||||||
specifier: 1.3.7
|
specifier: 2.0.1
|
||||||
version: 1.3.7
|
version: 2.0.1
|
||||||
node-pty:
|
node-pty:
|
||||||
specifier: 1.0.0
|
specifier: 1.0.0
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
@@ -473,9 +473,6 @@ importers:
|
|||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^18.19.104
|
specifier: ^18.19.104
|
||||||
version: 18.19.104
|
version: 18.19.104
|
||||||
'@types/node-os-utils':
|
|
||||||
specifier: 1.3.4
|
|
||||||
version: 1.3.4
|
|
||||||
'@types/node-schedule':
|
'@types/node-schedule':
|
||||||
specifier: 2.1.6
|
specifier: 2.1.6
|
||||||
version: 2.1.6
|
version: 2.1.6
|
||||||
@@ -688,8 +685,8 @@ importers:
|
|||||||
specifier: 3.3.11
|
specifier: 3.3.11
|
||||||
version: 3.3.11
|
version: 3.3.11
|
||||||
node-os-utils:
|
node-os-utils:
|
||||||
specifier: 1.3.7
|
specifier: 2.0.1
|
||||||
version: 1.3.7
|
version: 2.0.1
|
||||||
node-pty:
|
node-pty:
|
||||||
specifier: 1.0.0
|
specifier: 1.0.0
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
@@ -766,9 +763,6 @@ importers:
|
|||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^18.19.104
|
specifier: ^18.19.104
|
||||||
version: 18.19.104
|
version: 18.19.104
|
||||||
'@types/node-os-utils':
|
|
||||||
specifier: 1.3.4
|
|
||||||
version: 1.3.4
|
|
||||||
'@types/node-schedule':
|
'@types/node-schedule':
|
||||||
specifier: 2.1.6
|
specifier: 2.1.6
|
||||||
version: 2.1.6
|
version: 2.1.6
|
||||||
@@ -4000,9 +3994,6 @@ packages:
|
|||||||
'@types/mysql@2.15.26':
|
'@types/mysql@2.15.26':
|
||||||
resolution: {integrity: sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==}
|
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':
|
'@types/node-schedule@2.1.6':
|
||||||
resolution: {integrity: sha512-6AlZSUiNTdaVmH5jXYxX9YgmF1zfOlbjUqw0EllTBmZCnN1R5RR/m/u3No1OiWR05bnQ4jM4/+w4FcGvkAtnKQ==}
|
resolution: {integrity: sha512-6AlZSUiNTdaVmH5jXYxX9YgmF1zfOlbjUqw0EllTBmZCnN1R5RR/m/u3No1OiWR05bnQ4jM4/+w4FcGvkAtnKQ==}
|
||||||
|
|
||||||
@@ -6312,8 +6303,9 @@ packages:
|
|||||||
'@types/node':
|
'@types/node':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
node-os-utils@1.3.7:
|
node-os-utils@2.0.1:
|
||||||
resolution: {integrity: sha512-fvnX9tZbR7WfCG5BAy3yO/nCLyjVWD6MghEq0z5FDfN+ZXpLWNITBdbifxQkQ25ebr16G0N7eRWJisOcMEHG3Q==}
|
resolution: {integrity: sha512-rH2N3qHZETLhdgTGhMMCE8zU3gsWO4we1MFtrSiAI7tYWrnJRc6dk2PseV4co3Lb0v/MbRONLQI2biHQYbpTpg==}
|
||||||
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
node-pty@1.0.0:
|
node-pty@1.0.0:
|
||||||
resolution: {integrity: sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==}
|
resolution: {integrity: sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==}
|
||||||
@@ -11338,8 +11330,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 20.17.51
|
'@types/node': 20.17.51
|
||||||
|
|
||||||
'@types/node-os-utils@1.3.4': {}
|
|
||||||
|
|
||||||
'@types/node-schedule@2.1.6':
|
'@types/node-schedule@2.1.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 20.17.51
|
'@types/node': 20.17.51
|
||||||
@@ -13852,7 +13842,7 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 18.19.104
|
'@types/node': 18.19.104
|
||||||
|
|
||||||
node-os-utils@1.3.7: {}
|
node-os-utils@2.0.1: {}
|
||||||
|
|
||||||
node-pty@1.0.0:
|
node-pty@1.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
Reference in New Issue
Block a user