Compare commits

...

67 Commits

Author SHA1 Message Date
Mauricio Siu
eef874ecd4 Merge pull request #1784 from Dokploy/1782-valid-certificate-shows-how-expired
Refactor expiration date extraction logic in certificate utility to i…
2025-04-26 23:38:09 -06:00
Mauricio Siu
d6daa5677a Refactor expiration date extraction logic in certificate utility to improve handling of ASN.1 date formats. Update to correctly identify and parse "not after" dates for both UTCTime and GeneralizedTime formats. 2025-04-26 23:37:49 -06:00
Mauricio Siu
dc03ba73b3 Merge branch 'main' into canary 2025-04-26 23:29:51 -06:00
Mauricio Siu
5c2159f7b2 Merge pull request #1783 from Dokploy/1747-backup-issues-doesnt-list-all-files
Enhance backup restoration UI and API by adding file size formatting,…
2025-04-26 23:26:35 -06:00
Mauricio Siu
ffcdbcf046 Enhance backup restoration UI and API by adding file size formatting, improving search debounce timing, and updating file listing to include additional metadata. Refactor file handling to ensure proper path resolution and error handling during JSON parsing. 2025-04-26 23:23:51 -06:00
Mauricio Siu
c0b35efaca Merge pull request #1781 from Dokploy/1760-docker-compose-raw-editor-autocomplete-appending-to-previously-typed-characters
Refactor code editor completion logic to use explicit from/to paramet…
2025-04-26 19:25:27 -06:00
Mauricio Siu
22dee88e51 Refactor code editor completion logic to use explicit from/to parameters for insertion and selection handling 2025-04-26 19:25:05 -06:00
Mauricio Siu
1645f7e932 Merge pull request #1780 from Dokploy/1775-dokploy-unable-to-start-if-gotify-server-is-unreachable
Some checks are pending
Auto PR to main when version changes / create-pr (push) Waiting to run
Build Docker images / build-and-push-cloud-image (push) Waiting to run
Build Docker images / build-and-push-schedule-image (push) Waiting to run
Build Docker images / build-and-push-server-image (push) Waiting to run
Dokploy Docker Build / docker-amd (push) Waiting to run
Dokploy Docker Build / docker-arm (push) Waiting to run
Dokploy Docker Build / combine-manifests (push) Blocked by required conditions
Dokploy Docker Build / generate-release (push) Blocked by required conditions
autofix.ci / format (push) Waiting to run
Dokploy Monitoring Build / docker-amd (push) Waiting to run
Dokploy Monitoring Build / docker-arm (push) Waiting to run
Dokploy Monitoring Build / combine-manifests (push) Blocked by required conditions
Refactor notification sending in Dokploy restart process to include e…
2025-04-26 17:27:40 -06:00
Mauricio Siu
b4aeb6577e Refactor notification sending in Dokploy restart process to include error handling for Discord, Gotify, Telegram, and Slack notifications. 2025-04-26 17:25:10 -06:00
Mauricio Siu
fdd330ca19 Merge pull request #1779 from Dokploy/feat/allow-to-pass-hostname-to-dokploy-server
Update server configuration to include HOST variable and enhance serv…
2025-04-26 17:09:16 -06:00
Mauricio Siu
33de620893 Update server configuration to include HOST variable and enhance server start log message 2025-04-26 17:05:21 -06:00
Mauricio Siu
6518407c0c Merge pull request #1563 from yusoofsh/add-disable-recurse-submodules-option
Add option to disable recurse submodules
2025-04-26 16:52:05 -06:00
Mauricio Siu
6f47999a2e Merge pull request #1521 from yni9ht/fix-mount-update
refactor(mount): streamline mount update logic and improve readability
2025-04-26 16:50:09 -06:00
Mauricio Siu
fe69d5d405 Enhance Bitbucket provider and application tests by adding enableSubmodules field. This update includes the integration of a switch component in the UI and updates to the test files to reflect the new feature. 2025-04-26 16:46:50 -06:00
autofix-ci[bot]
a6880fd38c [autofix.ci] apply automated fixes 2025-04-26 22:45:14 +00:00
Mauricio Siu
5d25de13dd Merge branch 'canary' into fix-mount-update 2025-04-26 16:44:49 -06:00
Mauricio Siu
5611dcccfd refactor(mount): enhance updateMount function with transaction handling and improved error management 2025-04-26 16:44:40 -06:00
autofix-ci[bot]
e2a1882fe3 [autofix.ci] apply automated fixes 2025-04-26 22:35:49 +00:00
Mauricio Siu
ceb16ae9f7 Implement enableSubmodules feature across various Git provider components and update database schema. This change introduces a new boolean field enableSubmodules to control submodule behavior in Git operations, replacing the previous recurseSubmodules field. Updates include modifications to the UI components, API routers, and database schema to accommodate this new feature. 2025-04-26 16:35:02 -06:00
Mauricio Siu
1911b5b674 Merge branch 'canary' into add-disable-recurse-submodules-option 2025-04-26 16:10:59 -06:00
Mauricio Siu
6b818bbb51 Merge pull request #1737 from Walzen665/feature/ctrl-s-saving-1736
Add Ctrl+S keyboard shortcut to save compose file
2025-04-26 16:09:46 -06:00
Mauricio Siu
79796185d6 Merge pull request #1744 from barbarbar338/fix/web-server-pg-backup
Some checks are pending
Build Docker images / build-and-push-cloud-image (push) Waiting to run
Build Docker images / build-and-push-schedule-image (push) Waiting to run
Build Docker images / build-and-push-server-image (push) Waiting to run
Dokploy Docker Build / docker-amd (push) Waiting to run
Dokploy Docker Build / docker-arm (push) Waiting to run
Dokploy Docker Build / combine-manifests (push) Blocked by required conditions
Dokploy Docker Build / generate-release (push) Blocked by required conditions
Dokploy Monitoring Build / docker-amd (push) Waiting to run
Dokploy Monitoring Build / docker-arm (push) Waiting to run
Dokploy Monitoring Build / combine-manifests (push) Blocked by required conditions
fix(backup): handle multiple container IDs in backup command
2025-04-26 16:08:46 -06:00
Mauricio Siu
461d7c530a fix(restore): streamline container ID retrieval for database operations
Refactor the database restore process to consistently use a single container ID for the PostgreSQL container. This change enhances reliability by ensuring that commands are executed against the correct container, preventing potential errors from multiple matches.

Co-authored-by: Merloss 54235902+Merloss@users.noreply.github.com
2025-04-26 16:07:50 -06:00
Mauricio Siu
ade4b8dd1b Merge pull request #1749 from malko/template-helpers
Template helpers: Enhanced JWT generation
2025-04-26 16:00:06 -06:00
Mauricio Siu
f49a67f8df refactor(jwt generation): Simplify payload property assignments and secret initialization 2025-04-26 01:50:26 -06:00
autofix-ci[bot]
c3986d7a08 [autofix.ci] apply automated fixes 2025-04-26 07:40:07 +00:00
Mauricio Siu
0bf4e5560c Merge pull request #1771 from LexiconAlex/build/update-nixpacks-to-1.35.0
Some checks are pending
Auto PR to main when version changes / create-pr (push) Waiting to run
Build Docker images / build-and-push-cloud-image (push) Waiting to run
Build Docker images / build-and-push-schedule-image (push) Waiting to run
Build Docker images / build-and-push-server-image (push) Waiting to run
Dokploy Docker Build / docker-amd (push) Waiting to run
Dokploy Docker Build / docker-arm (push) Waiting to run
Dokploy Docker Build / combine-manifests (push) Blocked by required conditions
Dokploy Docker Build / generate-release (push) Blocked by required conditions
autofix.ci / format (push) Waiting to run
Dokploy Monitoring Build / docker-amd (push) Waiting to run
Dokploy Monitoring Build / docker-arm (push) Waiting to run
Dokploy Monitoring Build / combine-manifests (push) Blocked by required conditions
Update Nixpacks to 1.35.0
2025-04-26 01:12:03 -06:00
autofix-ci[bot]
79d55d8d34 [autofix.ci] apply automated fixes 2025-04-25 08:17:18 +00:00
Alexander Sjösten
d4c6e5b048 build: update nixpacks to 1.35.0 2025-04-25 09:58:52 +02:00
Mauricio Siu
cd4eed3507 Merge pull request #1769 from Dokploy/cloud/use-app-dokploy-instead-of-main-domain
Some checks failed
Auto PR to main when version changes / create-pr (push) Has been cancelled
Build Docker images / build-and-push-cloud-image (push) Has been cancelled
Build Docker images / build-and-push-schedule-image (push) Has been cancelled
Build Docker images / build-and-push-server-image (push) Has been cancelled
Dokploy Docker Build / docker-amd (push) Has been cancelled
Dokploy Docker Build / docker-arm (push) Has been cancelled
autofix.ci / format (push) Has been cancelled
Dokploy Monitoring Build / docker-amd (push) Has been cancelled
Dokploy Monitoring Build / docker-arm (push) Has been cancelled
Dokploy Docker Build / combine-manifests (push) Has been cancelled
Dokploy Docker Build / generate-release (push) Has been cancelled
Dokploy Monitoring Build / combine-manifests (push) Has been cancelled
fix(auth): update invite link host to use app.dokploy.com
2025-04-25 01:41:40 -06:00
Mauricio Siu
a650bd16fb Merge pull request #1770 from Dokploy/1742-400-error-on-settings-page
refactor(user-nav): remove settings dropdown for owner role
2025-04-25 01:41:33 -06:00
Mauricio Siu
4e5b5f219e fix(auth): update invite link host to use app.dokploy.com 2025-04-25 01:41:03 -06:00
Mauricio Siu
dfda934726 refactor(user-nav): remove settings dropdown for owner role 2025-04-25 01:38:14 -06:00
Jonathan Gotti
e6d0b7b4ee test(templates): Add test for jwt generation 2025-04-21 16:12:34 +02:00
Jonathan Gotti
d0dbc1837f feat(template-helpers): Add timestamps and timestampms helpers 2025-04-21 16:05:08 +02:00
Jonathan Gotti
2b5af1897f fix(template-helpers): hash not working without parameter 2025-04-21 16:04:00 +02:00
Jonathan Gotti
11b9cee73d feat(template-helpers): Add more parameters to jwt helper
- jwt without parameter now generate a real jwt
- keep length parameter as is for backward compatibility
- add secret and payload parameters
- payload properties iss, iat, exp are automaticly set if not provided
2025-04-21 16:00:16 +02:00
Jonathan Gotti
bc17991580 test: Add some template helpers test 2025-04-21 15:53:38 +02:00
Barış DEMİRCİ
8d28a50a17 fix(backup): handle multiple container IDs in backup command
Ensure only one container ID is used when running `docker exec` for pg_dump to avoid errors caused by multiple matching containers.

Fixes INTERNAL_SERVER_ERROR from backup.manualBackupWebServer path.

Co-authored-by: Merloss 54235902+Merloss@users.noreply.github.com
2025-04-20 12:14:41 +00:00
Max W.
08bbeceeba Add Ctrl+S keyboard shortcut to save compose file
https://github.com/Dokploy/dokploy/issues/1736
2025-04-19 16:10:35 +02:00
Yusoof Moh
b7bf09bf21 Merge remote-tracking branch 'upstream/canary' into add-disable-recurse-submodules-option 2025-04-19 15:27:13 +07:00
Mauricio Siu
546c6ade82 Merge pull request #1732 from nktnet1/fix-overflow-toolbar
Some checks failed
Auto PR to main when version changes / create-pr (push) Has been cancelled
Build Docker images / build-and-push-cloud-image (push) Has been cancelled
Build Docker images / build-and-push-schedule-image (push) Has been cancelled
Build Docker images / build-and-push-server-image (push) Has been cancelled
Dokploy Docker Build / docker-amd (push) Has been cancelled
Dokploy Docker Build / docker-arm (push) Has been cancelled
autofix.ci / format (push) Has been cancelled
Dokploy Monitoring Build / docker-amd (push) Has been cancelled
Dokploy Monitoring Build / docker-arm (push) Has been cancelled
Dokploy Docker Build / combine-manifests (push) Has been cancelled
Dokploy Docker Build / generate-release (push) Has been cancelled
Dokploy Monitoring Build / combine-manifests (push) Has been cancelled
Fix overflow toolbar
2025-04-18 03:52:14 -06:00
Khiet Tam Nguyen
db2e3691a5 fix: grid cols start from lg instead of md for compose 2025-04-18 13:01:43 +10:00
Khiet Tam Nguyen
a6dca144a8 fix: add overflow-x-scroll to tab list container 2025-04-18 12:54:42 +10:00
Khiet Tam Nguyen
43a17e7e75 style: remove double space 2025-04-18 12:49:02 +10:00
Mauricio Siu
da60c4f3a8 Merge pull request #1707 from Dokploy/canary
Some checks failed
Build Docker images / build-and-push-cloud-image (push) Has been cancelled
Build Docker images / build-and-push-schedule-image (push) Has been cancelled
Build Docker images / build-and-push-server-image (push) Has been cancelled
Dokploy Docker Build / docker-amd (push) Has been cancelled
Dokploy Docker Build / docker-arm (push) Has been cancelled
Dokploy Monitoring Build / docker-amd (push) Has been cancelled
Dokploy Monitoring Build / docker-arm (push) Has been cancelled
Dokploy Docker Build / combine-manifests (push) Has been cancelled
Dokploy Docker Build / generate-release (push) Has been cancelled
Dokploy Monitoring Build / combine-manifests (push) Has been cancelled
🚀 Release v0.21.7
2025-04-17 02:30:26 -06:00
Mauricio Siu
764f8ec993 Merge pull request #1695 from Dokploy/canary
🚀 Release v0.21.6
2025-04-13 00:05:04 -06:00
Mauricio Siu
ef7918a33a Merge pull request #1665 from Dokploy/canary
🚀 Release v0.21.5
2025-04-08 23:24:19 -06:00
Mauricio Siu
af4511040f Merge pull request #1645 from Dokploy/canary
🚀 Release v0.21.4
2025-04-06 17:07:04 -06:00
Mauricio Siu
1bbbdfba60 Merge pull request #1618 from Dokploy/canary
🚀 Release v0.21.3
2025-04-03 00:24:36 -06:00
Mauricio Siu
116e33ce37 Merge pull request #1609 from Dokploy/canary
🚀 Release v0.21.2
2025-04-02 07:22:22 -06:00
Mauricio Siu
e9b92d2641 Merge pull request #1600 from Dokploy/canary
🚀 Release v0.21.1
2025-04-02 00:39:10 -06:00
Yusoof Moh
96e9799afb Merge branch 'Dokploy:canary' into add-disable-recurse-submodules-option 2025-03-30 21:11:25 +07:00
Mauricio Siu
3e07be38df Merge pull request #1583 from Dokploy/canary
🚀 Release v0.21.0
2025-03-30 04:01:46 -06:00
autofix-ci[bot]
67e85cabcb [autofix.ci] apply automated fixes 2025-03-30 08:51:24 +00:00
Yusoof Moh
64a77decfd Merge branch 'Dokploy:canary' into add-disable-recurse-submodules-option 2025-03-30 12:02:06 +07:00
Yusoof Moh
cc5a3e6873 Add option to disable recurse submodules
Add option to disable recurse submodules under "Provider Select the source of your code" form.

* Add a checkbox to disable recurse submodules in `apps/dokploy/components/dashboard/application/general/generic/save-git-provider.tsx`, `apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx`, and `apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx`.
* Update the form schema in the above files to include the new option.
* Conditionally include the `--recurse-submodules` flag in the `git clone` command in the above files.
* Update the "Provider Select the source of your code" form in `apps/dokploy/components/dashboard/application/general/generic/show.tsx` to include the new option.
* Conditionally include the `--recurse-submodules` flag in the `git clone` command in `packages/server/src/utils/providers/bitbucket.ts`, `packages/server/src/utils/providers/git.ts`, `packages/server/src/utils/providers/github.ts`, and `packages/server/src/utils/providers/gitlab.ts`.
* Add the `--depth` flag to optimize submodule cloning performance in the `git clone` command in `packages/server/src/utils/providers/bitbucket.ts`, `packages/server/src/utils/providers/git.ts`, `packages/server/src/utils/providers/github.ts`, and `packages/server/src/utils/providers/gitlab.ts`.

---

For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/Dokploy/dokploy?shareId=XXXX-XXXX-XXXX-XXXX).
2025-03-25 22:04:35 +07:00
Mauricio Siu
b1d1763988 Merge pull request #1535 from Dokploy/canary
🚀 Release v0.20.8
2025-03-19 00:57:24 -06:00
Mauricio Siu
b5d199057d Merge pull request #1532 from Dokploy/canary
🚀 Release v0.20.7
2025-03-18 21:38:47 -06:00
Mauricio Siu
bfb6baf572 Merge pull request #1529 from Dokploy/canary
🚀 Release v0.20.6
2025-03-18 01:01:21 -06:00
yni9ht
371c6317aa refactor(mount): streamline mount update logic and improve readability 2025-03-17 20:44:13 +08:00
Mauricio Siu
1f81794904 Merge pull request #1517 from Dokploy/canary
refactor: improve button structure and tooltip integration across das…
2025-03-16 20:54:15 -06:00
Mauricio Siu
d5d3831d54 Merge pull request #1515 from Dokploy/canary
🚀 Release v0.20.5
2025-03-16 20:32:45 -06:00
Mauricio Siu
856399550a Merge pull request #1509 from Dokploy/canary
🚀 Release v0.20.4
2025-03-16 03:34:37 -06:00
Mauricio Siu
86b8b0987b Merge pull request #1505 from Dokploy/canary
🚀 Release v0.20.3
2025-03-16 00:18:54 -06:00
Mauricio Siu
0dac1fefe6 Merge pull request #1460 from Dokploy/canary
🚀 Release v0.20.2
2025-03-11 00:51:50 -06:00
Mauricio Siu
633ba899e0 Merge pull request #1454 from Dokploy/canary
🚀 Release v0.20.1
2025-03-10 03:26:01 -06:00
60 changed files with 11860 additions and 322 deletions

View File

@@ -52,7 +52,7 @@ feat: add new feature
Before you start, please make the clone based on the `canary` branch, since the `main` branch is the source of truth and should always reflect the latest stable release, also the PRs will be merged to the `canary` branch.
We use Node v20.9.0 and recommend this specific version. If you have nvm installed, you can run `nvm install 20.9.0 && nvm use` in the root directory.
We use Node v20.9.0 and recommend this specific version. If you have nvm installed, you can run `nvm install 20.9.0 && nvm use` in the root directory.
```bash
git clone https://github.com/dokploy/dokploy.git
@@ -147,11 +147,9 @@ curl -sSL https://railpack.com/install.sh | sh
```bash
# Install Buildpacks
curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.32.1/pack-v0.32.1-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v0.35.0-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
```
## Pull Request
- The `main` branch is the source of truth and should always reflect the latest stable release.
@@ -169,7 +167,6 @@ Thank you for your contribution!
To add a new template, go to `https://github.com/Dokploy/templates` repository and read the README.md file.
### Recommendations
- Use the same name of the folder as the id of the template.

View File

@@ -49,7 +49,7 @@ RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm
# Install Nixpacks and tsx
# | VERBOSE=1 VERSION=1.21.0 bash
ARG NIXPACKS_VERSION=1.29.1
ARG NIXPACKS_VERSION=1.35.0
RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
&& chmod +x install.sh \
&& ./install.sh \

View File

@@ -34,6 +34,7 @@ const baseApp: ApplicationNested = {
giteaRepository: "",
cleanCache: false,
watchPaths: [],
enableSubmodules: false,
applicationStatus: "done",
appName: "",
autoDeploy: true,

View File

@@ -51,6 +51,35 @@ describe("processTemplate", () => {
expect(result.domains).toHaveLength(0);
expect(result.mounts).toHaveLength(0);
});
it("should allow creation of real jwt secret", () => {
const template: CompleteTemplate = {
metadata: {} as any,
variables: {
jwt_secret: "cQsdycq1hDLopQonF6jUTqgQc5WEZTwWLL02J6XJ",
anon_payload: JSON.stringify({
role: "tester",
iss: "dockploy",
iat: "${timestamps:2025-01-01T00:00:00Z}",
exp: "${timestamps:2030-01-01T00:00:00Z}",
}),
anon_key: "${jwt:jwt_secret:anon_payload}",
},
config: {
domains: [],
env: {
ANON_KEY: "${anon_key}",
},
},
};
const result = processTemplate(template, mockSchema);
expect(result.envs).toHaveLength(1);
expect(result.envs).toContain(
"ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOiIxNzM1Njg5NjAwIiwiZXhwIjoiMTg5MzQ1NjAwMCIsInJvbGUiOiJ0ZXN0ZXIiLCJpc3MiOiJkb2NrcGxveSJ9.BG5JoxL2_NaTFbPgyZdm3kRWenf_O3su_HIRKGCJ_kY",
);
expect(result.mounts).toHaveLength(0);
expect(result.domains).toHaveLength(0);
});
});
describe("domains processing", () => {

View File

@@ -0,0 +1,232 @@
import type { Schema } from "@dokploy/server/templates";
import { processValue } from "@dokploy/server/templates/processors";
import { describe, expect, it } from "vitest";
describe("helpers functions", () => {
// Mock schema for testing
const mockSchema: Schema = {
projectName: "test",
serverIp: "127.0.0.1",
};
// some helpers to test jwt
type JWTParts = [string, string, string];
const jwtMatchExp = /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/;
const jwtBase64Decode = (str: string) => {
const base64 = str.replace(/-/g, "+").replace(/_/g, "/");
const padding = "=".repeat((4 - (base64.length % 4)) % 4);
const decoded = Buffer.from(base64 + padding, "base64").toString("utf-8");
return JSON.parse(decoded);
};
const jwtCheckHeader = (jwtHeader: string) => {
const decodedHeader = jwtBase64Decode(jwtHeader);
expect(decodedHeader).toHaveProperty("alg");
expect(decodedHeader).toHaveProperty("typ");
expect(decodedHeader.alg).toEqual("HS256");
expect(decodedHeader.typ).toEqual("JWT");
};
describe("${domain}", () => {
it("should generate a random domain", () => {
const domain = processValue("${domain}", {}, mockSchema);
expect(domain.startsWith(`${mockSchema.projectName}-`)).toBeTruthy();
expect(
domain.endsWith(
`${mockSchema.serverIp.replaceAll(".", "-")}.traefik.me`,
),
).toBeTruthy();
});
});
describe("${base64}", () => {
it("should generate a base64 string", () => {
const base64 = processValue("${base64}", {}, mockSchema);
expect(base64).toMatch(/^[A-Za-z0-9+=/]+={0,2}$/);
});
it.each([
[4, 8],
[8, 12],
[16, 24],
[32, 44],
[64, 88],
[128, 172],
])(
"should generate a base64 string from parameter %d bytes length",
(length, finalLength) => {
const base64 = processValue(`\${base64:${length}}`, {}, mockSchema);
expect(base64).toMatch(/^[A-Za-z0-9+=/]+={0,2}$/);
expect(base64.length).toBe(finalLength);
},
);
});
describe("${password}", () => {
it("should generate a password string", () => {
const password = processValue("${password}", {}, mockSchema);
expect(password).toMatch(/^[A-Za-z0-9]+$/);
});
it.each([6, 8, 12, 16, 32])(
"should generate a password string respecting parameter %d length",
(length) => {
const password = processValue(`\${password:${length}}`, {}, mockSchema);
expect(password).toMatch(/^[A-Za-z0-9]+$/);
expect(password.length).toBe(length);
},
);
});
describe("${hash}", () => {
it("should generate a hash string", () => {
const hash = processValue("${hash}", {}, mockSchema);
expect(hash).toMatch(/^[A-Za-z0-9]+$/);
});
it.each([6, 8, 12, 16, 32])(
"should generate a hash string respecting parameter %d length",
(length) => {
const hash = processValue(`\${hash:${length}}`, {}, mockSchema);
expect(hash).toMatch(/^[A-Za-z0-9]+$/);
expect(hash.length).toBe(length);
},
);
});
describe("${uuid}", () => {
it("should generate a UUID string", () => {
const uuid = processValue("${uuid}", {}, mockSchema);
expect(uuid).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
);
});
});
describe("${timestamp}", () => {
it("should generate a timestamp string in milliseconds", () => {
const timestamp = processValue("${timestamp}", {}, mockSchema);
const nowLength = Math.floor(Date.now()).toString().length;
expect(timestamp).toMatch(/^\d+$/);
expect(timestamp.length).toBe(nowLength);
});
});
describe("${timestampms}", () => {
it("should generate a timestamp string in milliseconds", () => {
const timestamp = processValue("${timestampms}", {}, mockSchema);
const nowLength = Date.now().toString().length;
expect(timestamp).toMatch(/^\d+$/);
expect(timestamp.length).toBe(nowLength);
});
it("should generate a timestamp string in milliseconds from parameter", () => {
const timestamp = processValue(
"${timestampms:2025-01-01}",
{},
mockSchema,
);
expect(timestamp).toEqual("1735689600000");
});
});
describe("${timestamps}", () => {
it("should generate a timestamp string in seconds", () => {
const timestamps = processValue("${timestamps}", {}, mockSchema);
const nowLength = Math.floor(Date.now() / 1000).toString().length;
expect(timestamps).toMatch(/^\d+$/);
expect(timestamps.length).toBe(nowLength);
});
it("should generate a timestamp string in seconds from parameter", () => {
const timestamps = processValue(
"${timestamps:2025-01-01}",
{},
mockSchema,
);
expect(timestamps).toEqual("1735689600");
});
});
describe("${randomPort}", () => {
it("should generate a random port string", () => {
const randomPort = processValue("${randomPort}", {}, mockSchema);
expect(randomPort).toMatch(/^\d+$/);
expect(Number(randomPort)).toBeLessThan(65536);
});
});
describe("${username}", () => {
it("should generate a username string", () => {
const username = processValue("${username}", {}, mockSchema);
expect(username).toMatch(/^[a-zA-Z0-9._-]{3,}$/);
});
});
describe("${email}", () => {
it("should generate an email string", () => {
const email = processValue("${email}", {}, mockSchema);
expect(email).toMatch(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/);
});
});
describe("${jwt}", () => {
it("should generate a JWT string", () => {
const jwt = processValue("${jwt}", {}, mockSchema);
expect(jwt).toMatch(jwtMatchExp);
const parts = jwt.split(".") as JWTParts;
const decodedPayload = jwtBase64Decode(parts[1]);
jwtCheckHeader(parts[0]);
expect(decodedPayload).toHaveProperty("iat");
expect(decodedPayload).toHaveProperty("iss");
expect(decodedPayload).toHaveProperty("exp");
expect(decodedPayload.iss).toEqual("dokploy");
});
it.each([6, 8, 12, 16, 32])(
"should generate a random hex string from parameter %d byte length",
(length) => {
const jwt = processValue(`\${jwt:${length}}`, {}, mockSchema);
expect(jwt).toMatch(/^[A-Za-z0-9-_.]+$/);
expect(jwt.length).toBeGreaterThanOrEqual(length); // bytes translated to hex can take up to 2x the length
expect(jwt.length).toBeLessThanOrEqual(length * 2);
},
);
});
describe("${jwt:secret}", () => {
it("should generate a JWT string respecting parameter secret from variable", () => {
const jwt = processValue(
"${jwt:secret}",
{ secret: "mysecret" },
mockSchema,
);
expect(jwt).toMatch(jwtMatchExp);
const parts = jwt.split(".") as JWTParts;
const decodedPayload = jwtBase64Decode(parts[1]);
jwtCheckHeader(parts[0]);
expect(decodedPayload).toHaveProperty("iat");
expect(decodedPayload).toHaveProperty("iss");
expect(decodedPayload).toHaveProperty("exp");
expect(decodedPayload.iss).toEqual("dokploy");
});
});
describe("${jwt:secret:payload}", () => {
it("should generate a JWT string respecting parameters secret and payload from variables", () => {
const iat = Math.floor(new Date("2025-01-01T00:00:00Z").getTime() / 1000);
const expiry = iat + 3600;
const jwt = processValue(
"${jwt:secret:payload}",
{
secret: "mysecret",
payload: `{"iss": "test-issuer", "iat": ${iat}, "exp": ${expiry}, "customprop": "customvalue"}`,
},
mockSchema,
);
expect(jwt).toMatch(jwtMatchExp);
const parts = jwt.split(".") as JWTParts;
jwtCheckHeader(parts[0]);
const decodedPayload = jwtBase64Decode(parts[1]);
expect(decodedPayload).toHaveProperty("iat");
expect(decodedPayload.iat).toEqual(iat);
expect(decodedPayload).toHaveProperty("iss");
expect(decodedPayload.iss).toEqual("test-issuer");
expect(decodedPayload).toHaveProperty("exp");
expect(decodedPayload.exp).toEqual(expiry);
expect(decodedPayload).toHaveProperty("customprop");
expect(decodedPayload.customprop).toEqual("customvalue");
expect(jwt).toEqual(
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MzU2ODk2MDAsImV4cCI6MTczNTY5MzIwMCwiaXNzIjoidGVzdC1pc3N1ZXIiLCJjdXN0b21wcm9wIjoiY3VzdG9tdmFsdWUifQ.m42U7PZSUSCf7gBOJrxJir0rQmyPq4rA59Dydr_QahI",
);
});
});
});

View File

@@ -16,6 +16,7 @@ const baseApp: ApplicationNested = {
applicationStatus: "done",
appName: "",
autoDeploy: true,
enableSubmodules: false,
serverId: "",
branch: null,
dockerBuildStage: "",

View File

@@ -31,6 +31,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import {
Tooltip,
TooltipContent,
@@ -58,6 +59,7 @@ const BitbucketProviderSchema = z.object({
branch: z.string().min(1, "Branch is required"),
bitbucketId: z.string().min(1, "Bitbucket Provider is required"),
watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().optional(),
});
type BitbucketProvider = z.infer<typeof BitbucketProviderSchema>;
@@ -84,6 +86,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
bitbucketId: "",
branch: "",
watchPaths: [],
enableSubmodules: false,
},
resolver: zodResolver(BitbucketProviderSchema),
});
@@ -130,6 +133,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
buildPath: data.bitbucketBuildPath || "/",
bitbucketId: data.bitbucketId || "",
watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules || false,
});
}
}, [form.reset, data, form]);
@@ -143,6 +147,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
bitbucketId: data.bitbucketId,
applicationId,
watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules || false,
})
.then(async () => {
toast.success("Service Provided Saved");
@@ -467,6 +472,21 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div>
<div className="flex w-full justify-end">
<Button

View File

@@ -23,6 +23,7 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
@@ -44,6 +45,7 @@ const GitProviderSchema = z.object({
branch: z.string().min(1, "Branch required"),
sshKey: z.string().optional(),
watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
});
type GitProvider = z.infer<typeof GitProviderSchema>;
@@ -67,6 +69,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
repositoryURL: "",
sshKey: undefined,
watchPaths: [],
enableSubmodules: false,
},
resolver: zodResolver(GitProviderSchema),
});
@@ -79,6 +82,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
buildPath: data.customGitBuildPath || "/",
repositoryURL: data.customGitUrl || "",
watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules ?? false,
});
}
}, [form.reset, data, form]);
@@ -91,6 +95,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
customGitSSHKeyId: values.sshKey === "none" ? null : values.sshKey,
applicationId,
watchPaths: values.watchPaths || [],
enableSubmodules: values.enableSubmodules,
})
.then(async () => {
toast.success("Git Provider Saved");
@@ -294,6 +299,22 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div>
<div className="flex flex-row justify-end">

View File

@@ -31,6 +31,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import {
Tooltip,
TooltipContent,
@@ -74,6 +75,7 @@ const GiteaProviderSchema = z.object({
branch: z.string().min(1, "Branch is required"),
giteaId: z.string().min(1, "Gitea Provider is required"),
watchPaths: z.array(z.string()).default([]),
enableSubmodules: z.boolean().optional(),
});
type GiteaProvider = z.infer<typeof GiteaProviderSchema>;
@@ -99,6 +101,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
giteaId: "",
branch: "",
watchPaths: [],
enableSubmodules: false,
},
resolver: zodResolver(GiteaProviderSchema),
});
@@ -152,6 +155,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
buildPath: data.giteaBuildPath || "/",
giteaId: data.giteaId || "",
watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules || false,
});
}
}, [form.reset, data, form]);
@@ -165,6 +169,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
giteaId: data.giteaId,
applicationId,
watchPaths: data.watchPaths,
enableSubmodules: data.enableSubmodules || false,
})
.then(async () => {
toast.success("Service Provider Saved");
@@ -498,6 +503,21 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div>
<div className="flex w-full justify-end">
<Button

View File

@@ -30,6 +30,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import {
Tooltip,
TooltipContent,
@@ -57,6 +58,7 @@ const GithubProviderSchema = z.object({
branch: z.string().min(1, "Branch is required"),
githubId: z.string().min(1, "Github Provider is required"),
watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
});
type GithubProvider = z.infer<typeof GithubProviderSchema>;
@@ -81,6 +83,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
},
githubId: "",
branch: "",
enableSubmodules: false,
},
resolver: zodResolver(GithubProviderSchema),
});
@@ -124,6 +127,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
buildPath: data.buildPath || "/",
githubId: data.githubId || "",
watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules ?? false,
});
}
}, [form.reset, data, form]);
@@ -137,6 +141,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
buildPath: data.buildPath,
githubId: data.githubId,
watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules,
})
.then(async () => {
toast.success("Service Provided Saved");
@@ -458,6 +463,22 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div>
<div className="flex w-full justify-end">
<Button

View File

@@ -31,6 +31,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import {
Tooltip,
TooltipContent,
@@ -60,6 +61,7 @@ const GitlabProviderSchema = z.object({
branch: z.string().min(1, "Branch is required"),
gitlabId: z.string().min(1, "Gitlab Provider is required"),
watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
});
type GitlabProvider = z.infer<typeof GitlabProviderSchema>;
@@ -86,6 +88,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
},
gitlabId: "",
branch: "",
enableSubmodules: false,
},
resolver: zodResolver(GitlabProviderSchema),
});
@@ -135,6 +138,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
buildPath: data.gitlabBuildPath || "/",
gitlabId: data.gitlabId || "",
watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules ?? false,
});
}
}, [form.reset, data, form]);
@@ -150,6 +154,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
gitlabProjectId: data.repository.id,
gitlabPathNamespace: data.repository.gitlabPathNamespace,
watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules,
})
.then(async () => {
toast.success("Service Provided Saved");
@@ -483,6 +488,21 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div>
<div className="flex w-full justify-end">
<Button

View File

@@ -65,7 +65,7 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
setSab(e as TabState);
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4">
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList className="md:grid md:w-fit md:grid-cols-7 max-md:overflow-x-scroll justify-start bg-transparent overflow-y-hidden">
<TabsTrigger
value="github"

View File

@@ -79,6 +79,22 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
toast.error("Error updating the Compose config");
});
};
// Add keyboard shortcut for Ctrl+S/Cmd+S
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isLoading) {
e.preventDefault();
form.handleSubmit(onSubmit)();
}
};
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
}, [form, onSubmit, isLoading]);
return (
<>
<div className="w-full flex flex-col gap-4 ">

View File

@@ -31,6 +31,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import {
Tooltip,
TooltipContent,
@@ -58,6 +59,7 @@ const BitbucketProviderSchema = z.object({
branch: z.string().min(1, "Branch is required"),
bitbucketId: z.string().min(1, "Bitbucket Provider is required"),
watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
});
type BitbucketProvider = z.infer<typeof BitbucketProviderSchema>;
@@ -84,6 +86,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
bitbucketId: "",
branch: "",
watchPaths: [],
enableSubmodules: false,
},
resolver: zodResolver(BitbucketProviderSchema),
});
@@ -130,6 +133,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
composePath: data.composePath,
bitbucketId: data.bitbucketId || "",
watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules ?? false,
});
}
}, [form.reset, data, form]);
@@ -145,6 +149,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
sourceType: "bitbucket",
composeStatus: "idle",
watchPaths: data.watchPaths,
enableSubmodules: data.enableSubmodules,
})
.then(async () => {
toast.success("Service Provided Saved");
@@ -469,6 +474,21 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div>
<div className="flex w-full justify-end">
<Button

View File

@@ -19,6 +19,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import {
Tooltip,
TooltipContent,
@@ -43,6 +44,7 @@ const GitProviderSchema = z.object({
branch: z.string().min(1, "Branch required"),
sshKey: z.string().optional(),
watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
});
type GitProvider = z.infer<typeof GitProviderSchema>;
@@ -65,6 +67,7 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
composePath: "./docker-compose.yml",
sshKey: undefined,
watchPaths: [],
enableSubmodules: false,
},
resolver: zodResolver(GitProviderSchema),
});
@@ -77,6 +80,7 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
repositoryURL: data.customGitUrl || "",
composePath: data.composePath,
watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules ?? false,
});
}
}, [form.reset, data, form]);
@@ -91,6 +95,7 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
composePath: values.composePath,
composeStatus: "idle",
watchPaths: values.watchPaths || [],
enableSubmodules: values.enableSubmodules,
})
.then(async () => {
toast.success("Git Provider Saved");
@@ -295,6 +300,21 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div>
<div className="flex flex-row justify-end">

View File

@@ -31,6 +31,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import {
Tooltip,
TooltipContent,
@@ -59,6 +60,7 @@ const GiteaProviderSchema = z.object({
branch: z.string().min(1, "Branch is required"),
giteaId: z.string().min(1, "Gitea Provider is required"),
watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
});
type GiteaProvider = z.infer<typeof GiteaProviderSchema>;
@@ -83,6 +85,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
giteaId: "",
branch: "",
watchPaths: [],
enableSubmodules: false,
},
resolver: zodResolver(GiteaProviderSchema),
});
@@ -136,6 +139,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
composePath: data.composePath || "./docker-compose.yml",
giteaId: data.giteaId || "",
watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules ?? false,
});
}
}, [form.reset, data, form]);
@@ -151,6 +155,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
sourceType: "gitea",
composeStatus: "idle",
watchPaths: data.watchPaths,
enableSubmodules: data.enableSubmodules,
} as any)
.then(async () => {
toast.success("Service Provider Saved");
@@ -469,6 +474,21 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div>
<div className="flex justify-end">

View File

@@ -30,6 +30,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import {
Tooltip,
TooltipContent,
@@ -57,6 +58,7 @@ const GithubProviderSchema = z.object({
branch: z.string().min(1, "Branch is required"),
githubId: z.string().min(1, "Github Provider is required"),
watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
});
type GithubProvider = z.infer<typeof GithubProviderSchema>;
@@ -82,6 +84,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
githubId: "",
branch: "",
watchPaths: [],
enableSubmodules: false,
},
resolver: zodResolver(GithubProviderSchema),
});
@@ -125,6 +128,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
composePath: data.composePath,
githubId: data.githubId || "",
watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules ?? false,
});
}
}, [form.reset, data, form]);
@@ -140,6 +144,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
sourceType: "github",
composeStatus: "idle",
watchPaths: data.watchPaths,
enableSubmodules: data.enableSubmodules,
})
.then(async () => {
toast.success("Service Provided Saved");
@@ -460,6 +465,21 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div>
<div className="flex w-full justify-end">
<Button

View File

@@ -31,6 +31,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import {
Tooltip,
TooltipContent,
@@ -60,6 +61,7 @@ const GitlabProviderSchema = z.object({
branch: z.string().min(1, "Branch is required"),
gitlabId: z.string().min(1, "Gitlab Provider is required"),
watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
});
type GitlabProvider = z.infer<typeof GitlabProviderSchema>;
@@ -87,6 +89,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
gitlabId: "",
branch: "",
watchPaths: [],
enableSubmodules: false,
},
resolver: zodResolver(GitlabProviderSchema),
});
@@ -136,6 +139,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
composePath: data.composePath,
gitlabId: data.gitlabId || "",
watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules ?? false,
});
}
}, [form.reset, data, form]);
@@ -153,6 +157,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
sourceType: "gitlab",
composeStatus: "idle",
watchPaths: data.watchPaths,
enableSubmodules: data.enableSubmodules,
})
.then(async () => {
toast.success("Service Provided Saved");
@@ -485,6 +490,21 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div>
<div className="flex w-full justify-end">
<Button

View File

@@ -77,6 +77,14 @@ const RestoreBackupSchema = z.object({
type RestoreBackup = z.infer<typeof RestoreBackupSchema>;
const formatBytes = (bytes: number): string => {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;
};
export const RestoreBackup = ({
databaseId,
databaseType,
@@ -101,7 +109,7 @@ export const RestoreBackup = ({
const debouncedSetSearch = debounce((value: string) => {
setDebouncedSearchTerm(value);
}, 150);
}, 350);
const handleSearchChange = (value: string) => {
setSearch(value);
@@ -271,7 +279,7 @@ export const RestoreBackup = ({
</Badge>
)}
</FormLabel>
<Popover>
<Popover modal>
<PopoverTrigger asChild>
<FormControl>
<Button
@@ -308,28 +316,51 @@ export const RestoreBackup = ({
</div>
) : (
<ScrollArea className="h-64">
<CommandGroup>
{files.map((file) => (
<CommandGroup className="w-96">
{files?.map((file) => (
<CommandItem
value={file}
key={file}
value={file.Path}
key={file.Path}
onSelect={() => {
form.setValue("backupFile", file);
setSearch(file);
setDebouncedSearchTerm(file);
form.setValue("backupFile", file.Path);
if (file.IsDir) {
setSearch(`${file.Path}/`);
setDebouncedSearchTerm(`${file.Path}/`);
} else {
setSearch(file.Path);
setDebouncedSearchTerm(file.Path);
}
}}
>
<div className="flex w-full justify-between">
<span>{file}</span>
<div className="flex w-full flex-col gap-1">
<div className="flex w-full justify-between">
<span className="font-medium">
{file.Path}
</span>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
file.Path === field.value
? "opacity-100"
: "opacity-0",
)}
/>
</div>
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<span>
Size: {formatBytes(file.Size)}
</span>
{file.IsDir && (
<span className="text-blue-500">
Directory
</span>
)}
{file.Hashes?.MD5 && (
<span>MD5: {file.Hashes.MD5}</span>
)}
</div>
</div>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
file === field.value
? "opacity-100"
: "opacity-0",
)}
/>
</CommandItem>
))}
</CommandGroup>

View File

@@ -55,7 +55,7 @@ export const AiForm = () => {
key={config.aiId}
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
>
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<div>
<span className="text-sm font-medium">
{config.name}

View File

@@ -70,7 +70,7 @@ export const ShowCertificates = () => {
key={certificate.certificateId}
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
>
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<div className="flex items-center justify-between">
<div className="flex gap-2 flex-col">
<span className="text-sm font-medium">

View File

@@ -13,53 +13,65 @@ export const extractExpirationDate = (certData: string): Date | null => {
bytes[i] = binaryStr.charCodeAt(i);
}
let dateFound = 0;
// ASN.1 tag for UTCTime is 0x17, GeneralizedTime is 0x18
// We need to find the second occurrence of either tag as it's the "not after" (expiration) date
let dateFound = false;
for (let i = 0; i < bytes.length - 2; i++) {
if (bytes[i] === 0x17 || bytes[i] === 0x18) {
const dateType = bytes[i];
const dateLength = bytes[i + 1];
if (typeof dateLength === "undefined") continue;
// Look for sequence containing validity period (0x30)
if (bytes[i] === 0x30) {
// Check next bytes for UTCTime or GeneralizedTime
let j = i + 1;
while (j < bytes.length - 2) {
if (bytes[j] === 0x17 || bytes[j] === 0x18) {
const dateType = bytes[j];
const dateLength = bytes[j + 1];
if (typeof dateLength === "undefined") break;
if (dateFound === 0) {
dateFound++;
i += dateLength + 1;
continue;
if (!dateFound) {
// Skip "not before" date
dateFound = true;
j += dateLength + 2;
continue;
}
// Found "not after" date
let dateStr = "";
for (let k = 0; k < dateLength; k++) {
const charCode = bytes[j + 2 + k];
if (typeof charCode === "undefined") continue;
dateStr += String.fromCharCode(charCode);
}
if (dateType === 0x17) {
// UTCTime (YYMMDDhhmmssZ)
const year = Number.parseInt(dateStr.slice(0, 2));
const fullYear = year >= 50 ? 1900 + year : 2000 + year;
return new Date(
Date.UTC(
fullYear,
Number.parseInt(dateStr.slice(2, 4)) - 1,
Number.parseInt(dateStr.slice(4, 6)),
Number.parseInt(dateStr.slice(6, 8)),
Number.parseInt(dateStr.slice(8, 10)),
Number.parseInt(dateStr.slice(10, 12)),
),
);
}
// GeneralizedTime (YYYYMMDDhhmmssZ)
return new Date(
Date.UTC(
Number.parseInt(dateStr.slice(0, 4)),
Number.parseInt(dateStr.slice(4, 6)) - 1,
Number.parseInt(dateStr.slice(6, 8)),
Number.parseInt(dateStr.slice(8, 10)),
Number.parseInt(dateStr.slice(10, 12)),
Number.parseInt(dateStr.slice(12, 14)),
),
);
}
j++;
}
let dateStr = "";
for (let j = 0; j < dateLength; j++) {
const charCode = bytes[i + 2 + j];
if (typeof charCode === "undefined") continue;
dateStr += String.fromCharCode(charCode);
}
if (dateType === 0x17) {
// UTCTime (YYMMDDhhmmssZ)
const year = Number.parseInt(dateStr.slice(0, 2));
const fullYear = year >= 50 ? 1900 + year : 2000 + year;
return new Date(
Date.UTC(
fullYear,
Number.parseInt(dateStr.slice(2, 4)) - 1,
Number.parseInt(dateStr.slice(4, 6)),
Number.parseInt(dateStr.slice(6, 8)),
Number.parseInt(dateStr.slice(8, 10)),
Number.parseInt(dateStr.slice(10, 12)),
),
);
}
// GeneralizedTime (YYYYMMDDhhmmssZ)
return new Date(
Date.UTC(
Number.parseInt(dateStr.slice(0, 4)),
Number.parseInt(dateStr.slice(4, 6)) - 1,
Number.parseInt(dateStr.slice(6, 8)),
Number.parseInt(dateStr.slice(8, 10)),
Number.parseInt(dateStr.slice(10, 12)),
Number.parseInt(dateStr.slice(12, 14)),
),
);
}
}
return null;

View File

@@ -54,7 +54,7 @@ export const ShowRegistry = () => {
key={registry.registryId}
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
>
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<div className="flex items-center justify-between">
<div className="flex gap-2 flex-col">
<span className="text-sm font-medium">

View File

@@ -55,7 +55,7 @@ export const ShowDestinations = () => {
key={destination.destinationId}
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
>
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<div className="flex flex-col gap-1">
<span className="text-sm">
{index + 1}. {destination.name}

View File

@@ -61,7 +61,7 @@ export const ShowNotifications = () => {
key={notification.notificationId}
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
>
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<span className="text-sm flex flex-row items-center gap-4">
{notification.notificationType === "slack" && (
<div className="flex items-center justify-center rounded-lg">

View File

@@ -56,7 +56,7 @@ export const ShowDestinations = () => {
key={sshKey.sshKeyId}
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
>
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<div className="flex items-center justify-between">
<div className="flex flex-col">
<span className="text-sm font-medium">

View File

@@ -133,17 +133,6 @@ export const UserNav = () => {
Servers
</DropdownMenuItem>
)}
{data?.role === "owner" && (
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
router.push("/dashboard/settings");
}}
>
Settings
</DropdownMenuItem>
)}
</>
)}
</DropdownMenuGroup>

View File

@@ -26,15 +26,20 @@ const dockerComposeServices = [
{ label: "secrets", type: "keyword", info: "Define secrets" },
].map((opt) => ({
...opt,
apply: (view: EditorView, completion: Completion) => {
apply: (
view: EditorView,
completion: Completion,
from: number,
to: number,
) => {
const insert = `${completion.label}:`;
view.dispatch({
changes: {
from: view.state.selection.main.from,
to: view.state.selection.main.to,
from,
to,
insert,
},
selection: { anchor: view.state.selection.main.from + insert.length },
selection: { anchor: from + insert.length },
});
},
}));
@@ -74,15 +79,20 @@ const dockerComposeServiceOptions = [
{ label: "networks", type: "keyword", info: "Networks to join" },
].map((opt) => ({
...opt,
apply: (view: EditorView, completion: Completion) => {
apply: (
view: EditorView,
completion: Completion,
from: number,
to: number,
) => {
const insert = `${completion.label}: `;
view.dispatch({
changes: {
from: view.state.selection.main.from,
to: view.state.selection.main.to,
from,
to,
insert,
},
selection: { anchor: view.state.selection.main.from + insert.length },
selection: { anchor: from + insert.length },
});
},
}));
@@ -99,6 +109,7 @@ function dockerComposeComplete(
const line = context.state.doc.lineAt(context.pos);
const indentation = /^\s*/.exec(line.text)?.[0].length || 0;
// If we're at the root level
if (indentation === 0) {
return {
from: word.from,

View File

@@ -0,0 +1,2 @@
ALTER TABLE "application" ADD COLUMN "enableSubmodules" boolean DEFAULT false;--> statement-breakpoint
ALTER TABLE "compose" ADD COLUMN "enableSubmodules" boolean DEFAULT false;

View File

@@ -0,0 +1,2 @@
ALTER TABLE "application" ALTER COLUMN "enableSubmodules" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "compose" ALTER COLUMN "enableSubmodules" SET NOT NULL;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -596,6 +596,20 @@
"when": 1743923992280,
"tag": "0084_thin_iron_lad",
"breakpoints": true
},
{
"idx": 85,
"version": "7",
"when": 1745705609181,
"tag": "0085_equal_captain_stacy",
"breakpoints": true
},
{
"idx": 86,
"version": "7",
"when": 1745706676004,
"tag": "0086_rainy_gertrude_yorkes",
"breakpoints": true
}
]
}

View File

@@ -215,7 +215,7 @@ const Service = (
router.push(newPath);
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4">
<div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll">
<TabsList
className={cn(
"flex gap-8 justify-start max-xl:overflow-x-scroll overflow-y-hidden",

View File

@@ -212,15 +212,15 @@ const Service = (
router.push(newPath);
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4">
<div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll">
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
"lg:grid lg:w-fit max-md:overflow-y-scroll justify-start",
isCloud && data?.serverId
? "md:grid-cols-7"
? "lg:grid-cols-7"
: data?.serverId
? "md:grid-cols-6"
: "md:grid-cols-7",
? "lg:grid-cols-6"
: "lg:grid-cols-7",
)}
>
<TabsTrigger value="general">General</TabsTrigger>

View File

@@ -182,7 +182,7 @@ const Mariadb = (
router.push(newPath, undefined, { shallow: true });
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4">
<div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll">
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",

View File

@@ -183,7 +183,7 @@ const Mongo = (
router.push(newPath, undefined, { shallow: true });
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4">
<div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll">
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",

View File

@@ -183,7 +183,7 @@ const MySql = (
router.push(newPath, undefined, { shallow: true });
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4">
<div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll">
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start ",

View File

@@ -182,7 +182,7 @@ const Postgresql = (
router.push(newPath, undefined, { shallow: true });
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4">
<div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll">
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",

View File

@@ -182,7 +182,7 @@ const Redis = (
router.push(newPath, undefined, { shallow: true });
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4">
<div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll">
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",

View File

@@ -355,6 +355,7 @@ export const applicationRouter = createTRPCRouter({
applicationStatus: "idle",
githubId: input.githubId,
watchPaths: input.watchPaths,
enableSubmodules: input.enableSubmodules,
});
return true;
@@ -382,6 +383,7 @@ export const applicationRouter = createTRPCRouter({
gitlabProjectId: input.gitlabProjectId,
gitlabPathNamespace: input.gitlabPathNamespace,
watchPaths: input.watchPaths,
enableSubmodules: input.enableSubmodules,
});
return true;
@@ -407,6 +409,7 @@ export const applicationRouter = createTRPCRouter({
applicationStatus: "idle",
bitbucketId: input.bitbucketId,
watchPaths: input.watchPaths,
enableSubmodules: input.enableSubmodules,
});
return true;
@@ -432,6 +435,7 @@ export const applicationRouter = createTRPCRouter({
applicationStatus: "idle",
giteaId: input.giteaId,
watchPaths: input.watchPaths,
enableSubmodules: input.enableSubmodules,
});
return true;
@@ -479,6 +483,7 @@ export const applicationRouter = createTRPCRouter({
sourceType: "git",
applicationStatus: "idle",
watchPaths: input.watchPaths,
enableSubmodules: input.enableSubmodules,
});
return true;

View File

@@ -50,6 +50,18 @@ import { TRPCError } from "@trpc/server";
import { observable } from "@trpc/server/observable";
import { z } from "zod";
interface RcloneFile {
Path: string;
Name: string;
Size: number;
IsDir: boolean;
Tier?: string;
Hashes?: {
MD5?: string;
SHA1?: string;
};
}
export const backupRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateBackup)
@@ -268,7 +280,7 @@ export const backupRouter = createTRPCRouter({
: input.search;
const searchPath = baseDir ? `${bucketPath}/${baseDir}` : bucketPath;
const listCommand = `rclone lsf ${rcloneFlags.join(" ")} "${searchPath}" | head -n 100`;
const listCommand = `rclone lsjson ${rcloneFlags.join(" ")} "${searchPath}" --no-mimetype --no-modtime 2>/dev/null`;
let stdout = "";
@@ -280,20 +292,35 @@ export const backupRouter = createTRPCRouter({
stdout = result.stdout;
}
const files = stdout.split("\n").filter(Boolean);
let files: RcloneFile[] = [];
try {
files = JSON.parse(stdout) as RcloneFile[];
} catch (error) {
console.error("Error parsing JSON response:", error);
console.error("Raw stdout:", stdout);
throw new Error("Failed to parse backup files list");
}
// Limit to first 100 files
const results = baseDir
? files.map((file) => `${baseDir}${file}`)
? files.map((file) => ({
...file,
Path: `${baseDir}${file.Path}`,
}))
: files;
if (searchTerm) {
return results.filter((file) =>
file.toLowerCase().includes(searchTerm.toLowerCase()),
);
return results
.filter((file) =>
file.Path.toLowerCase().includes(searchTerm.toLowerCase()),
)
.slice(0, 100);
}
return results;
return results.slice(0, 100);
} catch (error) {
console.error("Error in listBackupFiles:", error);
throw new TRPCError({
code: "BAD_REQUEST",
message:

View File

@@ -31,7 +31,6 @@ export const mountRouter = createTRPCRouter({
update: protectedProcedure
.input(apiUpdateMount)
.mutation(async ({ input }) => {
await updateMount(input.mountId, input);
return true;
return await updateMount(input.mountId, input);
}),
});

View File

@@ -21,6 +21,7 @@ import { setupTerminalWebSocketServer } from "./wss/terminal";
config({ path: ".env" });
const PORT = Number.parseInt(process.env.PORT || "3000", 10);
const HOST = process.env.HOST || "0.0.0.0";
const dev = process.env.NODE_ENV !== "production";
const app = next({ dev, turbopack: process.env.TURBOPACK === "1" });
const handle = app.getRequestHandler();
@@ -55,8 +56,8 @@ void app.prepare().then(async () => {
await migration();
}
server.listen(PORT);
console.log("Server Started:", PORT);
server.listen(PORT, HOST);
console.log(`Server Started on: http://${HOST}:${PORT}`);
if (!IS_CLOUD) {
console.log("Starting Deployment Worker");
const { deploymentWorker } = await import("./queues/deployments-queue");

View File

@@ -182,6 +182,7 @@ export const applications = pgTable("application", {
onDelete: "set null",
},
),
enableSubmodules: boolean("enableSubmodules").notNull().default(false),
dockerfile: text("dockerfile"),
dockerContextPath: text("dockerContextPath"),
dockerBuildStage: text("dockerBuildStage"),
@@ -470,6 +471,7 @@ export const apiSaveGithubProvider = createSchema
buildPath: true,
githubId: true,
watchPaths: true,
enableSubmodules: true,
})
.required();
@@ -484,6 +486,7 @@ export const apiSaveGitlabProvider = createSchema
gitlabProjectId: true,
gitlabPathNamespace: true,
watchPaths: true,
enableSubmodules: true,
})
.required();
@@ -496,6 +499,7 @@ export const apiSaveBitbucketProvider = createSchema
bitbucketId: true,
applicationId: true,
watchPaths: true,
enableSubmodules: true,
})
.required();
@@ -508,6 +512,7 @@ export const apiSaveGiteaProvider = createSchema
giteaRepository: true,
giteaId: true,
watchPaths: true,
enableSubmodules: true,
})
.required();
@@ -528,6 +533,7 @@ export const apiSaveGitProvider = createSchema
customGitBuildPath: true,
customGitUrl: true,
watchPaths: true,
enableSubmodules: true,
})
.required()
.merge(

View File

@@ -72,6 +72,7 @@ export const compose = pgTable("compose", {
),
command: text("command").notNull().default(""),
//
enableSubmodules: boolean("enableSubmodules").notNull().default(false),
composePath: text("composePath").notNull().default("./docker-compose.yml"),
suffix: text("suffix").notNull().default(""),
randomize: boolean("randomize").notNull().default(false),

View File

@@ -201,7 +201,7 @@ const { handler, api } = betterAuth({
const host =
process.env.NODE_ENV === "development"
? "http://localhost:3000"
: "https://dokploy.com";
: "https://app.dokploy.com";
const inviteLink = `${host}/invitation?token=${data.id}`;
await sendEmail({

View File

@@ -356,6 +356,7 @@ export const deployRemoteCompose = async ({
deployment.logPath,
true,
);
console.log(command);
} else if (compose.sourceType === "raw") {
command += getCreateComposeFileCommand(compose, deployment.logPath);
} else if (compose.sourceType === "gitea") {

View File

@@ -144,7 +144,8 @@ export const updateMount = async (
await deleteFileMount(mountId);
await createFileMount(mountId);
}
return mount;
return await findMountById(mountId);
});
};

View File

@@ -76,7 +76,7 @@ CURRENT_USER=$USER
echo "Installing requirements for: OS: $OS_TYPE"
if [ $EUID != 0 ]; then
echo "Please run this script as root or with sudo ❌"
echo "Please run this script as root or with sudo ❌"
exit
fi
@@ -263,7 +263,7 @@ const setupMainDirectory = () => `
# Create the /etc/dokploy directory
mkdir -p /etc/dokploy
chmod 777 /etc/dokploy
echo "Directory /etc/dokploy created ✅"
fi
`;
@@ -276,16 +276,16 @@ export const setupSwarm = () => `
# Get IP address
get_ip() {
local ip=""
# Try IPv4 with multiple services
# First attempt: ifconfig.io
ip=\$(curl -4s --connect-timeout 5 https://ifconfig.io 2>/dev/null)
# Second attempt: icanhazip.com
if [ -z "\$ip" ]; then
ip=\$(curl -4s --connect-timeout 5 https://icanhazip.com 2>/dev/null)
fi
# Third attempt: ipecho.net
if [ -z "\$ip" ]; then
ip=\$(curl -4s --connect-timeout 5 https://ipecho.net/plain 2>/dev/null)
@@ -295,12 +295,12 @@ export const setupSwarm = () => `
if [ -z "\$ip" ]; then
# Try IPv6 with ifconfig.io
ip=\$(curl -6s --connect-timeout 5 https://ifconfig.io 2>/dev/null)
# Try IPv6 with icanhazip.com
if [ -z "\$ip" ]; then
ip=\$(curl -6s --connect-timeout 5 https://icanhazip.com 2>/dev/null)
fi
# Try IPv6 with ipecho.net
if [ -z "\$ip" ]; then
ip=\$(curl -6s --connect-timeout 5 https://ipecho.net/plain 2>/dev/null)
@@ -549,7 +549,7 @@ export const createTraefikInstance = () => {
sleep 8
echo "Traefik migrated to Standalone ✅"
fi
if docker inspect dokploy-traefik > /dev/null 2>&1; then
echo "Traefik already exists ✅"
else
@@ -577,7 +577,7 @@ const installNixpacks = () => `
if command_exists nixpacks; then
echo "Nixpacks already installed ✅"
else
export NIXPACKS_VERSION=1.29.1
export NIXPACKS_VERSION=1.35.0
bash -c "$(curl -fsSL https://nixpacks.com/install.sh)"
echo "Nixpacks version $NIXPACKS_VERSION installed ✅"
fi

View File

@@ -1,4 +1,4 @@
import { randomBytes } from "node:crypto";
import { randomBytes, createHmac } from "node:crypto";
import { existsSync } from "node:fs";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
@@ -24,6 +24,12 @@ export interface Template {
domains: DomainSchema[];
}
export interface GenerateJWTOptions {
length?: number;
secret?: string;
payload?: Record<string, unknown> | undefined;
}
export const generateRandomDomain = ({
serverIp,
projectName,
@@ -61,8 +67,48 @@ export function generateBase64(bytes = 32): string {
return randomBytes(bytes).toString("base64");
}
export function generateJwt(length = 256): string {
return randomBytes(length).toString("hex");
function safeBase64(str: string): string {
return str.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
}
function objToJWTBase64(obj: any): string {
return safeBase64(
Buffer.from(JSON.stringify(obj), "utf8").toString("base64"),
);
}
export function generateJwt(options: GenerateJWTOptions = {}): string {
let { length, secret, payload = {} } = options;
if (length) {
return randomBytes(length).toString("hex");
}
const encodedHeader = objToJWTBase64({
alg: "HS256",
typ: "JWT",
});
if (!payload.iss) {
payload.iss = "dokploy";
}
if (!payload.iat) {
payload.iat = Math.floor(Date.now() / 1000);
}
if (!payload.exp) {
payload.exp = Math.floor(new Date("2030-01-01T00:00:00Z").getTime() / 1000);
}
const encodedPayload = objToJWTBase64({
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(new Date("2030-01-01T00:00:00Z").getTime() / 1000),
...payload,
});
if (!secret) {
secret = randomBytes(32).toString("hex");
}
const signature = safeBase64(
createHmac("SHA256", secret)
.update(`${encodedHeader}.${encodedPayload}`)
.digest("base64"),
);
return `${encodedHeader}.${encodedPayload}.${signature}`;
}
/**

View File

@@ -65,7 +65,7 @@ export interface Template {
/**
* Process a string value and replace variables
*/
function processValue(
export function processValue(
value: string,
variables: Record<string, string>,
schema: Schema,
@@ -84,11 +84,11 @@ function processValue(
const length = Number.parseInt(varName.split(":")[1], 10) || 32;
return generateBase64(length);
}
if (varName.startsWith("password:")) {
const length = Number.parseInt(varName.split(":")[1], 10) || 16;
return generatePassword(length);
}
if (varName === "password") {
return generatePassword(16);
}
@@ -97,14 +97,31 @@ function processValue(
const length = Number.parseInt(varName.split(":")[1], 10) || 8;
return generateHash(length);
}
if (varName === "hash") {
return generateHash();
}
if (varName === "uuid") {
return crypto.randomUUID();
}
if (varName === "timestamp") {
if (varName === "timestamp" || varName === "timestampms") {
return Date.now().toString();
}
if (varName === "timestamps") {
return Math.round(Date.now() / 1000).toString();
}
if (varName.startsWith("timestampms:")) {
return new Date(varName.slice(12)).getTime().toString();
}
if (varName.startsWith("timestamps:")) {
return Math.round(
new Date(varName.slice(11)).getTime() / 1000,
).toString();
}
if (varName === "randomPort") {
return Math.floor(Math.random() * 65535).toString();
}
@@ -114,8 +131,34 @@ function processValue(
}
if (varName.startsWith("jwt:")) {
const length = Number.parseInt(varName.split(":")[1], 10) || 256;
return generateJwt(length);
const params: string[] = varName.split(":").slice(1);
if (params.length === 1 && params[0] && params[0].match(/^\d{1,3}$/)) {
return generateJwt({ length: Number.parseInt(params[0], 10) });
}
let [secret, payload] = params;
if (typeof payload === "string" && variables[payload]) {
payload = variables[payload];
}
if (
typeof payload === "string" &&
payload.startsWith("{") &&
payload.endsWith("}")
) {
try {
payload = JSON.parse(payload);
} catch (e) {
// If payload is not a valid JSON, invalid it
payload = undefined;
console.error("Invalid JWT payload", e);
}
}
if (typeof payload !== "object") {
payload = undefined;
}
return generateJwt({
secret: secret ? variables[secret] || secret : undefined,
payload: payload as any,
});
}
if (varName === "username") {
@@ -147,7 +190,7 @@ export function processVariables(
): Record<string, string> {
const variables: Record<string, string> = {};
// First pass: Process variables that don't depend on other variables
// First pass: Process some variables that don't depend on other variables
for (const [key, value] of Object.entries(template.variables)) {
if (typeof value !== "string") continue;
@@ -161,6 +204,8 @@ export function processVariables(
const match = value.match(/\${password:(\d+)}/);
const length = match?.[1] ? Number.parseInt(match[1], 10) : 16;
variables[key] = generatePassword(length);
} else if (value === "${hash}") {
variables[key] = generateHash();
} else if (value.startsWith("${hash:")) {
const match = value.match(/\${hash:(\d+)}/);
const length = match?.[1] ? Number.parseInt(match[1], 10) : 8;

View File

@@ -25,21 +25,23 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
// First get the container ID
const { stdout: containerId } = await execAsync(
"docker ps --filter 'name=dokploy-postgres' -q",
`docker ps --filter "name=dokploy-postgres" --filter "status=running" -q | head -n 1`,
);
if (!containerId) {
throw new Error("PostgreSQL container not found");
}
// Then run pg_dump with the container ID
const postgresCommand = `docker exec ${containerId.trim()} pg_dump -v -Fc -U dokploy -d dokploy > '${tempDir}/database.sql'`;
const postgresContainerId = containerId.trim();
const postgresCommand = `docker exec ${postgresContainerId} pg_dump -v -Fc -U dokploy -d dokploy > '${tempDir}/database.sql'`;
await execAsync(postgresCommand);
await execAsync(`cp -r ${BASE_PATH}/* ${tempDir}/filesystem/`);
await execAsync(
`cd ${tempDir} && zip -r ${backupFileName} database.sql filesystem/ > /dev/null 2>&1`,
// Zip all .sql files since we created more than one
`cd ${tempDir} && zip -r ${backupFileName} *.sql filesystem/ > /dev/null 2>&1`,
);
const uploadCommand = `rclone copyto ${rcloneFlags.join(" ")} "${tempDir}/${backupFileName}" "${s3Path}"`;

View File

@@ -40,68 +40,84 @@ export const sendDokployRestartNotifications = async () => {
const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim();
await sendDiscordNotification(discord, {
title: decorate(">", "`✅` Dokploy Server Restarted"),
color: 0x57f287,
fields: [
{
name: decorate("`📅`", "Date"),
value: `<t:${unixDate}:D>`,
inline: true,
try {
await sendDiscordNotification(discord, {
title: decorate(">", "`✅` Dokploy Server Restarted"),
color: 0x57f287,
fields: [
{
name: decorate("`📅`", "Date"),
value: `<t:${unixDate}:D>`,
inline: true,
},
{
name: decorate("`⌚`", "Time"),
value: `<t:${unixDate}:t>`,
inline: true,
},
{
name: decorate("`❓`", "Type"),
value: "Successful",
inline: true,
},
],
timestamp: date.toISOString(),
footer: {
text: "Dokploy Restart Notification",
},
{
name: decorate("`⌚`", "Time"),
value: `<t:${unixDate}:t>`,
inline: true,
},
{
name: decorate("`❓`", "Type"),
value: "Successful",
inline: true,
},
],
timestamp: date.toISOString(),
footer: {
text: "Dokploy Restart Notification",
},
});
});
} catch (error) {
console.log(error);
}
}
if (gotify) {
const decorate = (decoration: string, text: string) =>
`${gotify.decoration ? decoration : ""} ${text}\n`;
await sendGotifyNotification(
gotify,
decorate("✅", "Dokploy Server Restarted"),
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}`,
);
try {
await sendGotifyNotification(
gotify,
decorate("", "Dokploy Server Restarted"),
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}`,
);
} catch (error) {
console.log(error);
}
}
if (telegram) {
await sendTelegramNotification(
telegram,
`<b>✅ Dokploy Server Restarted</b>\n\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`,
);
try {
await sendTelegramNotification(
telegram,
`<b>✅ Dokploy Server Restarted</b>\n\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`,
);
} catch (error) {
console.log(error);
}
}
if (slack) {
const { channel } = slack;
await sendSlackNotification(slack, {
channel: channel,
attachments: [
{
color: "#00FF00",
pretext: ":white_check_mark: *Dokploy Server Restarted*",
fields: [
{
title: "Time",
value: date.toLocaleString(),
short: true,
},
],
},
],
});
try {
await sendSlackNotification(slack, {
channel: channel,
attachments: [
{
color: "#00FF00",
pretext: ":white_check_mark: *Dokploy Server Restarted*",
fields: [
{
title: "Time",
value: date.toLocaleString(),
short: true,
},
],
},
],
});
} catch (error) {
console.log(error);
}
}
}
};

View File

@@ -37,6 +37,7 @@ export const cloneBitbucketRepository = async (
bitbucketBranch,
bitbucketId,
bitbucket,
enableSubmodules,
} = entity;
if (!bitbucketId) {
@@ -53,25 +54,23 @@ export const cloneBitbucketRepository = async (
const cloneUrl = `https://${bitbucket?.bitbucketUsername}:${bitbucket?.appPassword}@${repoclone}`;
try {
writeStream.write(`\nCloning Repo ${repoclone} to ${outputPath}: ✅\n`);
await spawnAsync(
"git",
[
"clone",
"--branch",
bitbucketBranch!,
"--depth",
"1",
"--recurse-submodules",
cloneUrl,
outputPath,
"--progress",
],
(data) => {
if (writeStream.writable) {
writeStream.write(data);
}
},
);
const cloneArgs = [
"clone",
"--branch",
bitbucketBranch!,
"--depth",
"1",
...(enableSubmodules ? ["--recurse-submodules"] : []),
cloneUrl,
outputPath,
"--progress",
];
await spawnAsync("git", cloneArgs, (data) => {
if (writeStream.writable) {
writeStream.write(data);
}
});
writeStream.write(`\nCloned ${repoclone} to ${outputPath}: ✅\n`);
} catch (error) {
writeStream.write(`ERROR Clonning: ${error}: ❌`);
@@ -89,6 +88,7 @@ export const cloneRawBitbucketRepository = async (entity: Compose) => {
bitbucketOwner,
bitbucketBranch,
bitbucketId,
enableSubmodules,
} = entity;
if (!bitbucketId) {
@@ -106,17 +106,19 @@ export const cloneRawBitbucketRepository = async (entity: Compose) => {
const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`;
try {
await spawnAsync("git", [
const cloneArgs = [
"clone",
"--branch",
bitbucketBranch!,
"--depth",
"1",
"--recurse-submodules",
...(enableSubmodules ? ["--recurse-submodules"] : []),
cloneUrl,
outputPath,
"--progress",
]);
];
await spawnAsync("git", cloneArgs);
} catch (error) {
throw error;
}
@@ -131,6 +133,7 @@ export const cloneRawBitbucketRepositoryRemote = async (compose: Compose) => {
bitbucketBranch,
bitbucketId,
serverId,
enableSubmodules,
} = compose;
if (!serverId) {
@@ -153,11 +156,11 @@ export const cloneRawBitbucketRepositoryRemote = async (compose: Compose) => {
const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`;
try {
const command = `
const cloneCommand = `
rm -rf ${outputPath};
git clone --branch ${bitbucketBranch} --depth 1 --recurse-submodules ${cloneUrl} ${outputPath}
git clone --branch ${bitbucketBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath}
`;
await execAsyncRemote(serverId, command);
await execAsyncRemote(serverId, cloneCommand);
} catch (error) {
throw error;
}
@@ -176,6 +179,7 @@ export const getBitbucketCloneCommand = async (
bitbucketBranch,
bitbucketId,
serverId,
enableSubmodules,
} = entity;
if (!serverId) {
@@ -207,7 +211,7 @@ export const getBitbucketCloneCommand = async (
const cloneCommand = `
rm -rf ${outputPath};
mkdir -p ${outputPath};
if ! git clone --branch ${bitbucketBranch} --depth 1 --recurse-submodules --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
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

View File

@@ -17,12 +17,19 @@ export const cloneGitRepository = async (
customGitUrl?: string | null;
customGitBranch?: string | null;
customGitSSHKeyId?: string | null;
enableSubmodules?: boolean;
},
logPath: string,
isCompose = false,
) => {
const { SSH_PATH, COMPOSE_PATH, APPLICATIONS_PATH } = paths();
const { appName, customGitUrl, customGitBranch, customGitSSHKeyId } = entity;
const {
appName,
customGitUrl,
customGitBranch,
customGitSSHKeyId,
enableSubmodules,
} = entity;
if (!customGitUrl || !customGitBranch) {
throw new TRPCError({
@@ -70,19 +77,21 @@ export const cloneGitRepository = async (
}
const { port } = sanitizeRepoPathSSH(customGitUrl);
const cloneArgs = [
"clone",
"--branch",
customGitBranch,
"--depth",
"1",
...(enableSubmodules ? ["--recurse-submodules"] : []),
customGitUrl,
outputPath,
"--progress",
];
await spawnAsync(
"git",
[
"clone",
"--branch",
customGitBranch,
"--depth",
"1",
"--recurse-submodules",
customGitUrl,
outputPath,
"--progress",
],
cloneArgs,
(data) => {
if (writeStream.writable) {
writeStream.write(data);
@@ -114,6 +123,7 @@ export const getCustomGitCloneCommand = async (
customGitBranch?: string | null;
customGitSSHKeyId?: string | null;
serverId: string | null;
enableSubmodules: boolean;
},
logPath: string,
isCompose = false,
@@ -125,6 +135,7 @@ export const getCustomGitCloneCommand = async (
customGitBranch,
customGitSSHKeyId,
serverId,
enableSubmodules,
} = entity;
if (!customGitUrl || !customGitBranch) {
@@ -181,7 +192,7 @@ export const getCustomGitCloneCommand = async (
}
command.push(
`if ! git clone --branch ${customGitBranch} --depth 1 --recurse-submodules --progress ${customGitUrl} ${outputPath} >> ${logPath} 2>&1; then
`if ! git clone --branch ${customGitBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${customGitUrl} ${outputPath} >> ${logPath} 2>&1; then
echo "❌ [ERROR] Fail to clone the repository ${customGitUrl}" >> ${logPath};
exit 1;
fi
@@ -261,8 +272,15 @@ export const cloneGitRawRepository = async (entity: {
customGitUrl?: string | null;
customGitBranch?: string | null;
customGitSSHKeyId?: string | null;
enableSubmodules?: boolean;
}) => {
const { appName, customGitUrl, customGitBranch, customGitSSHKeyId } = entity;
const {
appName,
customGitUrl,
customGitBranch,
customGitSSHKeyId,
enableSubmodules,
} = entity;
if (!customGitUrl || !customGitBranch) {
throw new TRPCError({
@@ -307,29 +325,26 @@ export const cloneGitRawRepository = async (entity: {
}
const { port } = sanitizeRepoPathSSH(customGitUrl);
await spawnAsync(
"git",
[
"clone",
"--branch",
customGitBranch,
"--depth",
"1",
"--recurse-submodules",
customGitUrl,
outputPath,
"--progress",
],
(_data) => {},
{
env: {
...process.env,
...(customGitSSHKeyId && {
GIT_SSH_COMMAND: `ssh -i ${temporalKeyPath}${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`,
}),
},
const cloneArgs = [
"clone",
"--branch",
customGitBranch,
"--depth",
"1",
...(enableSubmodules ? ["--recurse-submodules"] : []),
customGitUrl,
outputPath,
"--progress",
];
await spawnAsync("git", cloneArgs, (_data) => {}, {
env: {
...process.env,
...(customGitSSHKeyId && {
GIT_SSH_COMMAND: `ssh -i ${temporalKeyPath}${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`,
}),
},
);
});
} catch (error) {
throw error;
}
@@ -342,6 +357,7 @@ export const cloneRawGitRepositoryRemote = async (compose: Compose) => {
customGitUrl,
customGitSSHKeyId,
serverId,
enableSubmodules,
} = compose;
if (!serverId) {
@@ -396,7 +412,7 @@ export const cloneRawGitRepositoryRemote = async (compose: Compose) => {
}
command.push(
`if ! git clone --branch ${customGitBranch} --depth 1 --recurse-submodules --progress ${customGitUrl} ${outputPath} ; then
`if ! git clone --branch ${customGitBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${customGitUrl} ${outputPath} ; then
echo "[ERROR] Fail to clone the repository ";
exit 1;
fi

View File

@@ -119,6 +119,7 @@ export const getGiteaCloneCommand = async (
giteaRepository,
serverId,
gitea,
enableSubmodules,
} = entity;
if (!serverId) {
@@ -155,7 +156,7 @@ export const getGiteaCloneCommand = async (
rm -rf ${outputPath};
mkdir -p ${outputPath};
if ! git clone --branch ${giteaBranch} --depth 1 --recurse-submodules ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
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
@@ -174,7 +175,14 @@ export const cloneGiteaRepository = async (
const { APPLICATIONS_PATH, COMPOSE_PATH } = paths();
const writeStream = createWriteStream(logPath, { flags: "a" });
const { appName, giteaBranch, giteaId, giteaOwner, giteaRepository } = entity;
const {
appName,
giteaBranch,
giteaId,
giteaOwner,
giteaRepository,
enableSubmodules,
} = entity;
if (!giteaId) {
throw new TRPCError({
@@ -211,7 +219,7 @@ export const cloneGiteaRepository = async (
giteaBranch!,
"--depth",
"1",
"--recurse-submodules",
...(enableSubmodules ? ["--recurse-submodules"] : []),
cloneUrl,
outputPath,
"--progress",
@@ -232,7 +240,14 @@ export const cloneGiteaRepository = async (
};
export const cloneRawGiteaRepository = async (entity: Compose) => {
const { appName, giteaRepository, giteaOwner, giteaBranch, giteaId } = entity;
const {
appName,
giteaRepository,
giteaOwner,
giteaBranch,
giteaId,
enableSubmodules,
} = entity;
const { COMPOSE_PATH } = paths();
if (!giteaId) {
@@ -265,7 +280,7 @@ export const cloneRawGiteaRepository = async (entity: Compose) => {
giteaBranch!,
"--depth",
"1",
"--recurse-submodules",
...(enableSubmodules ? ["--recurse-submodules"] : []),
cloneUrl,
outputPath,
"--progress",
@@ -283,6 +298,7 @@ export const cloneRawGiteaRepositoryRemote = async (compose: Compose) => {
giteaBranch,
giteaId,
serverId,
enableSubmodules,
} = compose;
if (!serverId) {
@@ -307,7 +323,7 @@ export const cloneRawGiteaRepositoryRemote = async (compose: Compose) => {
try {
const command = `
rm -rf ${outputPath};
git clone --branch ${giteaBranch} --depth 1 ${cloneUrl} ${outputPath}
git clone --branch ${giteaBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath}
`;
await execAsyncRemote(serverId, command);
} catch (error) {

View File

@@ -83,6 +83,7 @@ interface CloneGithubRepository {
repository: string | null;
logPath: string;
type?: "application" | "compose";
enableSubmodules: boolean;
}
export const cloneGithubRepository = async ({
logPath,
@@ -92,7 +93,8 @@ export const cloneGithubRepository = async ({
const isCompose = type === "compose";
const { APPLICATIONS_PATH, COMPOSE_PATH } = paths();
const writeStream = createWriteStream(logPath, { flags: "a" });
const { appName, repository, owner, branch, githubId } = entity;
const { appName, repository, owner, branch, githubId, enableSubmodules } =
entity;
if (!githubId) {
throw new TRPCError({
@@ -128,25 +130,23 @@ export const cloneGithubRepository = async ({
try {
writeStream.write(`\nClonning Repo ${repoclone} to ${outputPath}: ✅\n`);
await spawnAsync(
"git",
[
"clone",
"--branch",
branch!,
"--depth",
"1",
"--recurse-submodules",
cloneUrl,
outputPath,
"--progress",
],
(data) => {
if (writeStream.writable) {
writeStream.write(data);
}
},
);
const cloneArgs = [
"clone",
"--branch",
branch!,
"--depth",
"1",
...(enableSubmodules ? ["--recurse-submodules"] : []),
cloneUrl,
outputPath,
"--progress",
];
await spawnAsync("git", cloneArgs, (data) => {
if (writeStream.writable) {
writeStream.write(data);
}
});
writeStream.write(`\nCloned ${repoclone}: ✅\n`);
} catch (error) {
writeStream.write(`ERROR Clonning: ${error}: ❌`);
@@ -161,7 +161,15 @@ export const getGithubCloneCommand = async ({
type = "application",
...entity
}: CloneGithubRepository & { serverId: string }) => {
const { appName, repository, owner, branch, githubId, serverId } = entity;
const {
appName,
repository,
owner,
branch,
githubId,
serverId,
enableSubmodules,
} = entity;
const isCompose = type === "compose";
if (!serverId) {
throw new TRPCError({
@@ -216,7 +224,7 @@ export const getGithubCloneCommand = async ({
const cloneCommand = `
rm -rf ${outputPath};
mkdir -p ${outputPath};
if ! git clone --branch ${branch} --depth 1 --recurse-submodules --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
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
@@ -227,7 +235,8 @@ echo "Cloned ${repoclone} to ${outputPath}: ✅" >> ${logPath};
};
export const cloneRawGithubRepository = async (entity: Compose) => {
const { appName, repository, owner, branch, githubId } = entity;
const { appName, repository, owner, branch, githubId, enableSubmodules } =
entity;
if (!githubId) {
throw new TRPCError({
@@ -245,24 +254,33 @@ export const cloneRawGithubRepository = async (entity: Compose) => {
await recreateDirectory(outputPath);
const cloneUrl = `https://oauth2:${token}@${repoclone}`;
try {
await spawnAsync("git", [
const cloneArgs = [
"clone",
"--branch",
branch!,
"--depth",
"1",
"--recurse-submodules",
...(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 } = compose;
const {
appName,
repository,
owner,
branch,
githubId,
serverId,
enableSubmodules,
} = compose;
if (!serverId) {
throw new TRPCError({
@@ -288,7 +306,7 @@ export const cloneRawGithubRepositoryRemote = async (compose: Compose) => {
try {
const command = `
rm -rf ${outputPath};
git clone --branch ${branch} --depth 1 ${cloneUrl} ${outputPath}
git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath}
`;
await execAsyncRemote(serverId, command);
} catch (error) {

View File

@@ -90,8 +90,14 @@ export const cloneGitlabRepository = async (
isCompose = false,
) => {
const writeStream = createWriteStream(logPath, { flags: "a" });
const { appName, gitlabBranch, gitlabId, gitlab, gitlabPathNamespace } =
entity;
const {
appName,
gitlabBranch,
gitlabId,
gitlab,
gitlabPathNamespace,
enableSubmodules,
} = entity;
if (!gitlabId) {
throw new TRPCError({
@@ -127,25 +133,23 @@ export const cloneGitlabRepository = async (
try {
writeStream.write(`\nClonning Repo ${repoclone} to ${outputPath}: ✅\n`);
await spawnAsync(
"git",
[
"clone",
"--branch",
gitlabBranch!,
"--depth",
"1",
"--recurse-submodules",
cloneUrl,
outputPath,
"--progress",
],
(data) => {
if (writeStream.writable) {
writeStream.write(data);
}
},
);
const cloneArgs = [
"clone",
"--branch",
gitlabBranch!,
"--depth",
"1",
...(enableSubmodules ? ["--recurse-submodules"] : []),
cloneUrl,
outputPath,
"--progress",
];
await spawnAsync("git", cloneArgs, (data) => {
if (writeStream.writable) {
writeStream.write(data);
}
});
writeStream.write(`\nCloned ${repoclone}: ✅\n`);
} catch (error) {
writeStream.write(`ERROR Clonning: ${error}: ❌`);
@@ -167,6 +171,7 @@ export const getGitlabCloneCommand = async (
gitlabId,
serverId,
gitlab,
enableSubmodules,
} = entity;
if (!serverId) {
@@ -222,7 +227,7 @@ export const getGitlabCloneCommand = async (
const cloneCommand = `
rm -rf ${outputPath};
mkdir -p ${outputPath};
if ! git clone --branch ${gitlabBranch} --depth 1 --recurse-submodules --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
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
@@ -330,7 +335,13 @@ export const getGitlabBranches = async (input: {
};
export const cloneRawGitlabRepository = async (entity: Compose) => {
const { appName, gitlabBranch, gitlabId, gitlabPathNamespace } = entity;
const {
appName,
gitlabBranch,
gitlabId,
gitlabPathNamespace,
enableSubmodules,
} = entity;
if (!gitlabId) {
throw new TRPCError({
@@ -351,24 +362,32 @@ export const cloneRawGitlabRepository = async (entity: Compose) => {
const cloneUrl = `https://oauth2:${gitlabProvider?.accessToken}@${repoclone}`;
try {
await spawnAsync("git", [
const cloneArgs = [
"clone",
"--branch",
gitlabBranch!,
"--depth",
"1",
"--recurse-submodules",
...(enableSubmodules ? ["--recurse-submodules"] : []),
cloneUrl,
outputPath,
"--progress",
]);
];
await spawnAsync("git", cloneArgs);
} catch (error) {
throw error;
}
};
export const cloneRawGitlabRepositoryRemote = async (compose: Compose) => {
const { appName, gitlabPathNamespace, branch, gitlabId, serverId } = compose;
const {
appName,
gitlabPathNamespace,
branch,
gitlabId,
serverId,
enableSubmodules,
} = compose;
if (!serverId) {
throw new TRPCError({
@@ -392,7 +411,7 @@ export const cloneRawGitlabRepositoryRemote = async (compose: Compose) => {
try {
const command = `
rm -rf ${outputPath};
git clone --branch ${branch} --depth 1 --recurse-submodules ${cloneUrl} ${outputPath}
git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath}
`;
await execAsyncRemote(serverId, command);
} catch (error) {

View File

@@ -83,44 +83,54 @@ export const restoreWebServerBackup = async (
throw new Error("Database file not found after extraction");
}
const { stdout: postgresContainer } = await execAsync(
`docker ps --filter "name=dokploy-postgres" --filter "status=running" -q | head -n 1`,
);
if (!postgresContainer) {
throw new Error("Dokploy Postgres container not found");
}
const postgresContainerId = postgresContainer.trim();
// Drop and recreate database
emit("Disconnecting all users from database...");
await execAsync(
`docker exec $(docker ps --filter "name=dokploy-postgres" -q) psql -U dokploy postgres -c "SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = 'dokploy' AND pid <> pg_backend_pid();"`,
`docker exec ${postgresContainerId} psql -U dokploy postgres -c "SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = 'dokploy' AND pid <> pg_backend_pid();"`,
);
emit("Dropping existing database...");
await execAsync(
`docker exec $(docker ps --filter "name=dokploy-postgres" -q) psql -U dokploy postgres -c "DROP DATABASE IF EXISTS dokploy;"`,
`docker exec ${postgresContainerId} psql -U dokploy postgres -c "DROP DATABASE IF EXISTS dokploy;"`,
);
emit("Creating fresh database...");
await execAsync(
`docker exec $(docker ps --filter "name=dokploy-postgres" -q) psql -U dokploy postgres -c "CREATE DATABASE dokploy;"`,
`docker exec ${postgresContainerId} psql -U dokploy postgres -c "CREATE DATABASE dokploy;"`,
);
// Copy the backup file into the container
emit("Copying backup file into container...");
await execAsync(
`docker cp ${tempDir}/database.sql $(docker ps --filter "name=dokploy-postgres" -q):/tmp/database.sql`,
`docker cp ${tempDir}/database.sql ${postgresContainerId}:/tmp/database.sql`,
);
// Verify file in container
emit("Verifying file in container...");
await execAsync(
`docker exec $(docker ps --filter "name=dokploy-postgres" -q) ls -l /tmp/database.sql`,
`docker exec ${postgresContainerId} ls -l /tmp/database.sql`,
);
// Restore from the copied file
emit("Running database restore...");
await execAsync(
`docker exec $(docker ps --filter "name=dokploy-postgres" -q) pg_restore -v -U dokploy -d dokploy /tmp/database.sql`,
`docker exec ${postgresContainerId} pg_restore -v -U dokploy -d dokploy /tmp/database.sql`,
);
// Cleanup the temporary file in the container
emit("Cleaning up container temp file...");
await execAsync(
`docker exec $(docker ps --filter "name=dokploy-postgres" -q) rm /tmp/database.sql`,
`docker exec ${postgresContainerId} rm /tmp/database.sql`,
);
emit("Restore completed successfully!");