Merge branch 'canary' into separate-permission-for-creating-environments-#2593

This commit is contained in:
Mauricio Siu
2025-10-05 00:08:49 -06:00
114 changed files with 28039 additions and 756 deletions

View File

@@ -1,7 +1,7 @@
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToAllProperties } from "@dokploy/server"; import { addSuffixToAllProperties } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";
import { parse } from "yaml";
const composeFile1 = ` const composeFile1 = `
version: "3.8" version: "3.8"
@@ -61,7 +61,7 @@ secrets:
file: ./db_password.txt file: ./db_password.txt
`; `;
const expectedComposeFile1 = load(` const expectedComposeFile1 = parse(`
version: "3.8" version: "3.8"
services: services:
@@ -120,7 +120,7 @@ secrets:
`) as ComposeSpecification; `) as ComposeSpecification;
test("Add suffix to all properties in compose file 1", () => { test("Add suffix to all properties in compose file 1", () => {
const composeData = load(composeFile1) as ComposeSpecification; const composeData = parse(composeFile1) as ComposeSpecification;
const suffix = "testhash"; const suffix = "testhash";
const updatedComposeData = addSuffixToAllProperties(composeData, suffix); const updatedComposeData = addSuffixToAllProperties(composeData, suffix);
@@ -185,7 +185,7 @@ secrets:
file: ./db_password.txt file: ./db_password.txt
`; `;
const expectedComposeFile2 = load(` const expectedComposeFile2 = parse(`
version: "3.8" version: "3.8"
services: services:
@@ -243,7 +243,7 @@ secrets:
`) as ComposeSpecification; `) as ComposeSpecification;
test("Add suffix to all properties in compose file 2", () => { test("Add suffix to all properties in compose file 2", () => {
const composeData = load(composeFile2) as ComposeSpecification; const composeData = parse(composeFile2) as ComposeSpecification;
const suffix = "testhash"; const suffix = "testhash";
const updatedComposeData = addSuffixToAllProperties(composeData, suffix); const updatedComposeData = addSuffixToAllProperties(composeData, suffix);
@@ -308,7 +308,7 @@ secrets:
file: ./service_secret.txt file: ./service_secret.txt
`; `;
const expectedComposeFile3 = load(` const expectedComposeFile3 = parse(`
version: "3.8" version: "3.8"
services: services:
@@ -366,7 +366,7 @@ secrets:
`) as ComposeSpecification; `) as ComposeSpecification;
test("Add suffix to all properties in compose file 3", () => { test("Add suffix to all properties in compose file 3", () => {
const composeData = load(composeFile3) as ComposeSpecification; const composeData = parse(composeFile3) as ComposeSpecification;
const suffix = "testhash"; const suffix = "testhash";
const updatedComposeData = addSuffixToAllProperties(composeData, suffix); const updatedComposeData = addSuffixToAllProperties(composeData, suffix);
@@ -420,7 +420,7 @@ volumes:
driver: local driver: local
`; `;
const expectedComposeFile = load(` const expectedComposeFile = parse(`
version: "3.8" version: "3.8"
services: services:
@@ -467,7 +467,7 @@ volumes:
`) as ComposeSpecification; `) as ComposeSpecification;
test("Add suffix to all properties in Plausible compose file", () => { test("Add suffix to all properties in Plausible compose file", () => {
const composeData = load(composeFile) as ComposeSpecification; const composeData = parse(composeFile) as ComposeSpecification;
const suffix = "testhash"; const suffix = "testhash";
const updatedComposeData = addSuffixToAllProperties(composeData, suffix); const updatedComposeData = addSuffixToAllProperties(composeData, suffix);

View File

@@ -1,7 +1,7 @@
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToConfigsRoot, generateRandomHash } from "@dokploy/server"; import { addSuffixToConfigsRoot, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";
import { parse } from "yaml";
test("Generate random hash with 8 characters", () => { test("Generate random hash with 8 characters", () => {
const hash = generateRandomHash(); const hash = generateRandomHash();
@@ -23,7 +23,7 @@ configs:
`; `;
test("Add suffix to configs in root property", () => { test("Add suffix to configs in root property", () => {
const composeData = load(composeFile) as ComposeSpecification; const composeData = parse(composeFile) as ComposeSpecification;
const suffix = generateRandomHash(); const suffix = generateRandomHash();
@@ -59,7 +59,7 @@ configs:
`; `;
test("Add suffix to multiple configs in root property", () => { test("Add suffix to multiple configs in root property", () => {
const composeData = load(composeFileMultipleConfigs) as ComposeSpecification; const composeData = parse(composeFileMultipleConfigs) as ComposeSpecification;
const suffix = generateRandomHash(); const suffix = generateRandomHash();
@@ -92,7 +92,7 @@ configs:
`; `;
test("Add suffix to configs with different properties in root property", () => { test("Add suffix to configs with different properties in root property", () => {
const composeData = load( const composeData = parse(
composeFileDifferentProperties, composeFileDifferentProperties,
) as ComposeSpecification; ) as ComposeSpecification;
@@ -137,7 +137,7 @@ configs:
`; `;
// Expected compose file con el prefijo `testhash` // Expected compose file con el prefijo `testhash`
const expectedComposeFileConfigRoot = load(` const expectedComposeFileConfigRoot = parse(`
version: "3.8" version: "3.8"
services: services:
@@ -162,7 +162,7 @@ configs:
`) as ComposeSpecification; `) as ComposeSpecification;
test("Add suffix to configs in root property", () => { test("Add suffix to configs in root property", () => {
const composeData = load(composeFileConfigRoot) as ComposeSpecification; const composeData = parse(composeFileConfigRoot) as ComposeSpecification;
const suffix = "testhash"; const suffix = "testhash";

View File

@@ -3,8 +3,8 @@ import {
addSuffixToConfigsInServices, addSuffixToConfigsInServices,
generateRandomHash, generateRandomHash,
} from "@dokploy/server"; } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";
import { parse } from "yaml";
const composeFile = ` const composeFile = `
version: "3.8" version: "3.8"
@@ -22,7 +22,7 @@ configs:
`; `;
test("Add suffix to configs in services", () => { test("Add suffix to configs in services", () => {
const composeData = load(composeFile) as ComposeSpecification; const composeData = parse(composeFile) as ComposeSpecification;
const suffix = generateRandomHash(); const suffix = generateRandomHash();
@@ -54,7 +54,7 @@ configs:
`; `;
test("Add suffix to configs in services with single config", () => { test("Add suffix to configs in services with single config", () => {
const composeData = load( const composeData = parse(
composeFileSingleServiceConfig, composeFileSingleServiceConfig,
) as ComposeSpecification; ) as ComposeSpecification;
@@ -108,7 +108,7 @@ configs:
`; `;
test("Add suffix to configs in services with multiple configs", () => { test("Add suffix to configs in services with multiple configs", () => {
const composeData = load( const composeData = parse(
composeFileMultipleServicesConfigs, composeFileMultipleServicesConfigs,
) as ComposeSpecification; ) as ComposeSpecification;
@@ -157,7 +157,7 @@ services:
`; `;
// Expected compose file con el prefijo `testhash` // Expected compose file con el prefijo `testhash`
const expectedComposeFileConfigServices = load(` const expectedComposeFileConfigServices = parse(`
version: "3.8" version: "3.8"
services: services:
@@ -182,7 +182,7 @@ services:
`) as ComposeSpecification; `) as ComposeSpecification;
test("Add suffix to configs in services", () => { test("Add suffix to configs in services", () => {
const composeData = load(composeFileConfigServices) as ComposeSpecification; const composeData = parse(composeFileConfigServices) as ComposeSpecification;
const suffix = "testhash"; const suffix = "testhash";

View File

@@ -1,7 +1,7 @@
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToAllConfigs, generateRandomHash } from "@dokploy/server"; import { addSuffixToAllConfigs, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";
import { parse } from "yaml";
test("Generate random hash with 8 characters", () => { test("Generate random hash with 8 characters", () => {
const hash = generateRandomHash(); const hash = generateRandomHash();
@@ -43,7 +43,7 @@ configs:
file: ./db-config.yml file: ./db-config.yml
`; `;
const expectedComposeFileCombinedConfigs = load(` const expectedComposeFileCombinedConfigs = parse(`
version: "3.8" version: "3.8"
services: services:
@@ -77,7 +77,7 @@ configs:
`) as ComposeSpecification; `) as ComposeSpecification;
test("Add suffix to all configs in root and services", () => { test("Add suffix to all configs in root and services", () => {
const composeData = load(composeFileCombinedConfigs) as ComposeSpecification; const composeData = parse(composeFileCombinedConfigs) as ComposeSpecification;
const suffix = "testhash"; const suffix = "testhash";
@@ -122,7 +122,7 @@ configs:
file: ./db-config.yml file: ./db-config.yml
`; `;
const expectedComposeFileWithEnvAndExternal = load(` const expectedComposeFileWithEnvAndExternal = parse(`
version: "3.8" version: "3.8"
services: services:
@@ -159,7 +159,7 @@ configs:
`) as ComposeSpecification; `) as ComposeSpecification;
test("Add suffix to configs with environment and external", () => { test("Add suffix to configs with environment and external", () => {
const composeData = load( const composeData = parse(
composeFileWithEnvAndExternal, composeFileWithEnvAndExternal,
) as ComposeSpecification; ) as ComposeSpecification;
@@ -200,7 +200,7 @@ configs:
file: ./db-config.yml file: ./db-config.yml
`; `;
const expectedComposeFileWithTemplateDriverAndLabels = load(` const expectedComposeFileWithTemplateDriverAndLabels = parse(`
version: "3.8" version: "3.8"
services: services:
@@ -231,7 +231,7 @@ configs:
`) as ComposeSpecification; `) as ComposeSpecification;
test("Add suffix to configs with template driver and labels", () => { test("Add suffix to configs with template driver and labels", () => {
const composeData = load( const composeData = parse(
composeFileWithTemplateDriverAndLabels, composeFileWithTemplateDriverAndLabels,
) as ComposeSpecification; ) as ComposeSpecification;

View File

@@ -1,7 +1,7 @@
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToNetworksRoot, generateRandomHash } from "@dokploy/server"; import { addSuffixToNetworksRoot, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";
import { parse } from "yaml";
const composeFile = ` const composeFile = `
version: "3.8" version: "3.8"
@@ -35,7 +35,7 @@ test("Generate random hash with 8 characters", () => {
}); });
test("Add suffix to networks root property", () => { test("Add suffix to networks root property", () => {
const composeData = load(composeFile) as ComposeSpecification; const composeData = parse(composeFile) as ComposeSpecification;
const suffix = generateRandomHash(); const suffix = generateRandomHash();
@@ -79,7 +79,7 @@ networks:
`; `;
test("Add suffix to advanced networks root property (2 TRY)", () => { test("Add suffix to advanced networks root property (2 TRY)", () => {
const composeData = load(composeFile2) as ComposeSpecification; const composeData = parse(composeFile2) as ComposeSpecification;
const suffix = generateRandomHash(); const suffix = generateRandomHash();
@@ -120,7 +120,7 @@ networks:
`; `;
test("Add suffix to networks with external properties", () => { test("Add suffix to networks with external properties", () => {
const composeData = load(composeFile3) as ComposeSpecification; const composeData = parse(composeFile3) as ComposeSpecification;
const suffix = generateRandomHash(); const suffix = generateRandomHash();
@@ -160,7 +160,7 @@ networks:
`; `;
test("Add suffix to networks with IPAM configurations", () => { test("Add suffix to networks with IPAM configurations", () => {
const composeData = load(composeFile4) as ComposeSpecification; const composeData = parse(composeFile4) as ComposeSpecification;
const suffix = generateRandomHash(); const suffix = generateRandomHash();
@@ -201,7 +201,7 @@ networks:
`; `;
test("Add suffix to networks with custom options", () => { test("Add suffix to networks with custom options", () => {
const composeData = load(composeFile5) as ComposeSpecification; const composeData = parse(composeFile5) as ComposeSpecification;
const suffix = generateRandomHash(); const suffix = generateRandomHash();
@@ -264,7 +264,7 @@ networks:
`; `;
test("Add suffix to networks with static suffix", () => { test("Add suffix to networks with static suffix", () => {
const composeData = load(composeFile6) as ComposeSpecification; const composeData = parse(composeFile6) as ComposeSpecification;
const suffix = "testhash"; const suffix = "testhash";
@@ -273,7 +273,7 @@ test("Add suffix to networks with static suffix", () => {
} }
const networks = addSuffixToNetworksRoot(composeData.networks, suffix); const networks = addSuffixToNetworksRoot(composeData.networks, suffix);
const expectedComposeData = load( const expectedComposeData = parse(
expectedComposeFile6, expectedComposeFile6,
) as ComposeSpecification; ) as ComposeSpecification;
expect(networks).toStrictEqual(expectedComposeData.networks); expect(networks).toStrictEqual(expectedComposeData.networks);
@@ -293,7 +293,7 @@ networks:
`; `;
test("It shoudn't add suffix to dokploy-network", () => { test("It shoudn't add suffix to dokploy-network", () => {
const composeData = load(composeFile7) as ComposeSpecification; const composeData = parse(composeFile7) as ComposeSpecification;
const suffix = generateRandomHash(); const suffix = generateRandomHash();

View File

@@ -3,8 +3,8 @@ import {
addSuffixToServiceNetworks, addSuffixToServiceNetworks,
generateRandomHash, generateRandomHash,
} from "@dokploy/server"; } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";
import { parse } from "yaml";
const composeFile = ` const composeFile = `
version: "3.8" version: "3.8"
@@ -23,7 +23,7 @@ services:
`; `;
test("Add suffix to networks in services", () => { test("Add suffix to networks in services", () => {
const composeData = load(composeFile) as ComposeSpecification; const composeData = parse(composeFile) as ComposeSpecification;
const suffix = generateRandomHash(); const suffix = generateRandomHash();
@@ -67,7 +67,7 @@ networks:
`; `;
test("Add suffix to networks in services with aliases", () => { test("Add suffix to networks in services with aliases", () => {
const composeData = load(composeFile2) as ComposeSpecification; const composeData = parse(composeFile2) as ComposeSpecification;
const suffix = generateRandomHash(); const suffix = generateRandomHash();
@@ -107,7 +107,7 @@ networks:
`; `;
test("Add suffix to networks in services (Object with simple networks)", () => { test("Add suffix to networks in services (Object with simple networks)", () => {
const composeData = load(composeFile3) as ComposeSpecification; const composeData = parse(composeFile3) as ComposeSpecification;
const suffix = generateRandomHash(); const suffix = generateRandomHash();
@@ -153,7 +153,7 @@ networks:
`; `;
test("Add suffix to networks in services (combined case)", () => { test("Add suffix to networks in services (combined case)", () => {
const composeData = load(composeFileCombined) as ComposeSpecification; const composeData = parse(composeFileCombined) as ComposeSpecification;
const suffix = generateRandomHash(); const suffix = generateRandomHash();
@@ -196,7 +196,7 @@ services:
`; `;
test("It shoudn't add suffix to dokploy-network in services", () => { test("It shoudn't add suffix to dokploy-network in services", () => {
const composeData = load(composeFile7) as ComposeSpecification; const composeData = parse(composeFile7) as ComposeSpecification;
const suffix = generateRandomHash(); const suffix = generateRandomHash();
@@ -245,7 +245,7 @@ services:
`; `;
test("It shoudn't add suffix to dokploy-network in services multiples cases", () => { test("It shoudn't add suffix to dokploy-network in services multiples cases", () => {
const composeData = load(composeFile8) as ComposeSpecification; const composeData = parse(composeFile8) as ComposeSpecification;
const suffix = generateRandomHash(); const suffix = generateRandomHash();

View File

@@ -5,8 +5,8 @@ import {
addSuffixToServiceNetworks, addSuffixToServiceNetworks,
generateRandomHash, generateRandomHash,
} from "@dokploy/server"; } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";
import { parse } from "yaml";
const composeFileCombined = ` const composeFileCombined = `
version: "3.8" version: "3.8"
@@ -39,7 +39,7 @@ networks:
`; `;
test("Add suffix to networks in services and root (combined case)", () => { test("Add suffix to networks in services and root (combined case)", () => {
const composeData = load(composeFileCombined) as ComposeSpecification; const composeData = parse(composeFileCombined) as ComposeSpecification;
const suffix = generateRandomHash(); const suffix = generateRandomHash();
@@ -89,7 +89,7 @@ test("Add suffix to networks in services and root (combined case)", () => {
expect(redisNetworks).not.toHaveProperty("backend"); expect(redisNetworks).not.toHaveProperty("backend");
}); });
const expectedComposeFile = load(` const expectedComposeFile = parse(`
version: "3.8" version: "3.8"
services: services:
@@ -120,7 +120,7 @@ networks:
`); `);
test("Add suffix to networks in compose file", () => { test("Add suffix to networks in compose file", () => {
const composeData = load(composeFileCombined) as ComposeSpecification; const composeData = parse(composeFileCombined) as ComposeSpecification;
const suffix = "testhash"; const suffix = "testhash";
if (!composeData?.networks) { if (!composeData?.networks) {
@@ -156,7 +156,7 @@ networks:
driver: bridge driver: bridge
`; `;
const expectedComposeFile2 = load(` const expectedComposeFile2 = parse(`
version: "3.8" version: "3.8"
services: services:
@@ -182,7 +182,7 @@ networks:
`); `);
test("Add suffix to networks in compose file with external and internal networks", () => { test("Add suffix to networks in compose file with external and internal networks", () => {
const composeData = load(composeFile2) as ComposeSpecification; const composeData = parse(composeFile2) as ComposeSpecification;
const suffix = "testhash"; const suffix = "testhash";
const updatedComposeData = addSuffixToAllNetworks(composeData, suffix); const updatedComposeData = addSuffixToAllNetworks(composeData, suffix);
@@ -218,7 +218,7 @@ networks:
com.docker.network.bridge.enable_icc: "true" com.docker.network.bridge.enable_icc: "true"
`; `;
const expectedComposeFile3 = load(` const expectedComposeFile3 = parse(`
version: "3.8" version: "3.8"
services: services:
@@ -247,7 +247,7 @@ networks:
`); `);
test("Add suffix to networks in compose file with multiple services and complex network configurations", () => { test("Add suffix to networks in compose file with multiple services and complex network configurations", () => {
const composeData = load(composeFile3) as ComposeSpecification; const composeData = parse(composeFile3) as ComposeSpecification;
const suffix = "testhash"; const suffix = "testhash";
const updatedComposeData = addSuffixToAllNetworks(composeData, suffix); const updatedComposeData = addSuffixToAllNetworks(composeData, suffix);
@@ -289,7 +289,7 @@ networks:
`; `;
const expectedComposeFile4 = load(` const expectedComposeFile4 = parse(`
version: "3.8" version: "3.8"
services: services:
@@ -326,7 +326,7 @@ networks:
`); `);
test("Expect don't add suffix to dokploy-network in compose file with multiple services and complex network configurations", () => { test("Expect don't add suffix to dokploy-network in compose file with multiple services and complex network configurations", () => {
const composeData = load(composeFile4) as ComposeSpecification; const composeData = parse(composeFile4) as ComposeSpecification;
const suffix = "testhash"; const suffix = "testhash";
const updatedComposeData = addSuffixToAllNetworks(composeData, suffix); const updatedComposeData = addSuffixToAllNetworks(composeData, suffix);

View File

@@ -1,7 +1,7 @@
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToSecretsRoot, generateRandomHash } from "@dokploy/server"; import { addSuffixToSecretsRoot, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";
import { parse } from "yaml";
test("Generate random hash with 8 characters", () => { test("Generate random hash with 8 characters", () => {
const hash = generateRandomHash(); const hash = generateRandomHash();
@@ -23,7 +23,7 @@ secrets:
`; `;
test("Add suffix to secrets in root property", () => { test("Add suffix to secrets in root property", () => {
const composeData = load(composeFileSecretsRoot) as ComposeSpecification; const composeData = parse(composeFileSecretsRoot) as ComposeSpecification;
const suffix = generateRandomHash(); const suffix = generateRandomHash();
if (!composeData?.secrets) { if (!composeData?.secrets) {
@@ -52,7 +52,7 @@ secrets:
`; `;
test("Add suffix to secrets in root property (Test 1)", () => { test("Add suffix to secrets in root property (Test 1)", () => {
const composeData = load(composeFileSecretsRoot1) as ComposeSpecification; const composeData = parse(composeFileSecretsRoot1) as ComposeSpecification;
const suffix = generateRandomHash(); const suffix = generateRandomHash();
if (!composeData?.secrets) { if (!composeData?.secrets) {
@@ -84,7 +84,7 @@ secrets:
`; `;
test("Add suffix to secrets in root property (Test 2)", () => { test("Add suffix to secrets in root property (Test 2)", () => {
const composeData = load(composeFileSecretsRoot2) as ComposeSpecification; const composeData = parse(composeFileSecretsRoot2) as ComposeSpecification;
const suffix = generateRandomHash(); const suffix = generateRandomHash();
if (!composeData?.secrets) { if (!composeData?.secrets) {

View File

@@ -3,8 +3,8 @@ import {
addSuffixToSecretsInServices, addSuffixToSecretsInServices,
generateRandomHash, generateRandomHash,
} from "@dokploy/server"; } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";
import { parse } from "yaml";
const composeFileSecretsServices = ` const composeFileSecretsServices = `
version: "3.8" version: "3.8"
@@ -21,7 +21,7 @@ secrets:
`; `;
test("Add suffix to secrets in services", () => { test("Add suffix to secrets in services", () => {
const composeData = load(composeFileSecretsServices) as ComposeSpecification; const composeData = parse(composeFileSecretsServices) as ComposeSpecification;
const suffix = generateRandomHash(); const suffix = generateRandomHash();
if (!composeData.services) { if (!composeData.services) {
@@ -54,7 +54,9 @@ secrets:
`; `;
test("Add suffix to secrets in services (Test 1)", () => { test("Add suffix to secrets in services (Test 1)", () => {
const composeData = load(composeFileSecretsServices1) as ComposeSpecification; const composeData = parse(
composeFileSecretsServices1,
) as ComposeSpecification;
const suffix = generateRandomHash(); const suffix = generateRandomHash();
if (!composeData.services) { if (!composeData.services) {
@@ -93,7 +95,9 @@ secrets:
`; `;
test("Add suffix to secrets in services (Test 2)", () => { test("Add suffix to secrets in services (Test 2)", () => {
const composeData = load(composeFileSecretsServices2) as ComposeSpecification; const composeData = parse(
composeFileSecretsServices2,
) as ComposeSpecification;
const suffix = generateRandomHash(); const suffix = generateRandomHash();
if (!composeData.services) { if (!composeData.services) {

View File

@@ -1,7 +1,7 @@
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToAllSecrets } from "@dokploy/server"; import { addSuffixToAllSecrets } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";
import { parse } from "yaml";
const composeFileCombinedSecrets = ` const composeFileCombinedSecrets = `
version: "3.8" version: "3.8"
@@ -25,7 +25,7 @@ secrets:
file: ./app_secret.txt file: ./app_secret.txt
`; `;
const expectedComposeFileCombinedSecrets = load(` const expectedComposeFileCombinedSecrets = parse(`
version: "3.8" version: "3.8"
services: services:
@@ -48,7 +48,7 @@ secrets:
`) as ComposeSpecification; `) as ComposeSpecification;
test("Add suffix to all secrets", () => { test("Add suffix to all secrets", () => {
const composeData = load(composeFileCombinedSecrets) as ComposeSpecification; const composeData = parse(composeFileCombinedSecrets) as ComposeSpecification;
const suffix = "testhash"; const suffix = "testhash";
const updatedComposeData = addSuffixToAllSecrets(composeData, suffix); const updatedComposeData = addSuffixToAllSecrets(composeData, suffix);
@@ -77,7 +77,7 @@ secrets:
file: ./cache_secret.txt file: ./cache_secret.txt
`; `;
const expectedComposeFileCombinedSecrets3 = load(` const expectedComposeFileCombinedSecrets3 = parse(`
version: "3.8" version: "3.8"
services: services:
@@ -99,7 +99,9 @@ secrets:
`) as ComposeSpecification; `) as ComposeSpecification;
test("Add suffix to all secrets (3rd Case)", () => { test("Add suffix to all secrets (3rd Case)", () => {
const composeData = load(composeFileCombinedSecrets3) as ComposeSpecification; const composeData = parse(
composeFileCombinedSecrets3,
) as ComposeSpecification;
const suffix = "testhash"; const suffix = "testhash";
const updatedComposeData = addSuffixToAllSecrets(composeData, suffix); const updatedComposeData = addSuffixToAllSecrets(composeData, suffix);
@@ -128,7 +130,7 @@ secrets:
file: ./db_password.txt file: ./db_password.txt
`; `;
const expectedComposeFileCombinedSecrets4 = load(` const expectedComposeFileCombinedSecrets4 = parse(`
version: "3.8" version: "3.8"
services: services:
@@ -150,7 +152,9 @@ secrets:
`) as ComposeSpecification; `) as ComposeSpecification;
test("Add suffix to all secrets (4th Case)", () => { test("Add suffix to all secrets (4th Case)", () => {
const composeData = load(composeFileCombinedSecrets4) as ComposeSpecification; const composeData = parse(
composeFileCombinedSecrets4,
) as ComposeSpecification;
const suffix = "testhash"; const suffix = "testhash";
const updatedComposeData = addSuffixToAllSecrets(composeData, suffix); const updatedComposeData = addSuffixToAllSecrets(composeData, suffix);

View File

@@ -1,7 +1,7 @@
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server"; import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";
import { parse } from "yaml";
const composeFile = ` const composeFile = `
version: "3.8" version: "3.8"
@@ -27,7 +27,7 @@ test("Generate random hash with 8 characters", () => {
}); });
test("Add suffix to service names with container_name in compose file", () => { test("Add suffix to service names with container_name in compose file", () => {
const composeData = load(composeFile) as ComposeSpecification; const composeData = parse(composeFile) as ComposeSpecification;
const suffix = generateRandomHash(); const suffix = generateRandomHash();

View File

@@ -1,7 +1,7 @@
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server"; import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";
import { parse } from "yaml";
test("Generate random hash with 8 characters", () => { test("Generate random hash with 8 characters", () => {
const hash = generateRandomHash(); const hash = generateRandomHash();
@@ -32,7 +32,7 @@ networks:
`; `;
test("Add suffix to service names with depends_on (array) in compose file", () => { test("Add suffix to service names with depends_on (array) in compose file", () => {
const composeData = load(composeFile4) as ComposeSpecification; const composeData = parse(composeFile4) as ComposeSpecification;
const suffix = generateRandomHash(); const suffix = generateRandomHash();
@@ -102,7 +102,7 @@ networks:
`; `;
test("Add suffix to service names with depends_on (object) in compose file", () => { test("Add suffix to service names with depends_on (object) in compose file", () => {
const composeData = load(composeFile5) as ComposeSpecification; const composeData = parse(composeFile5) as ComposeSpecification;
const suffix = generateRandomHash(); const suffix = generateRandomHash();

View File

@@ -1,7 +1,7 @@
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server"; import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";
import { parse } from "yaml";
test("Generate random hash with 8 characters", () => { test("Generate random hash with 8 characters", () => {
const hash = generateRandomHash(); const hash = generateRandomHash();
@@ -30,7 +30,7 @@ networks:
`; `;
test("Add suffix to service names with extends (string) in compose file", () => { test("Add suffix to service names with extends (string) in compose file", () => {
const composeData = load(composeFile6) as ComposeSpecification; const composeData = parse(composeFile6) as ComposeSpecification;
const suffix = generateRandomHash(); const suffix = generateRandomHash();
@@ -90,7 +90,7 @@ networks:
`; `;
test("Add suffix to service names with extends (object) in compose file", () => { test("Add suffix to service names with extends (object) in compose file", () => {
const composeData = load(composeFile7) as ComposeSpecification; const composeData = parse(composeFile7) as ComposeSpecification;
const suffix = generateRandomHash(); const suffix = generateRandomHash();

View File

@@ -1,7 +1,7 @@
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server"; import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";
import { parse } from "yaml";
test("Generate random hash with 8 characters", () => { test("Generate random hash with 8 characters", () => {
const hash = generateRandomHash(); const hash = generateRandomHash();
@@ -31,7 +31,7 @@ networks:
`; `;
test("Add suffix to service names with links in compose file", () => { test("Add suffix to service names with links in compose file", () => {
const composeData = load(composeFile2) as ComposeSpecification; const composeData = parse(composeFile2) as ComposeSpecification;
const suffix = generateRandomHash(); const suffix = generateRandomHash();

View File

@@ -1,7 +1,7 @@
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server"; import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";
import { parse } from "yaml";
test("Generate random hash with 8 characters", () => { test("Generate random hash with 8 characters", () => {
const hash = generateRandomHash(); const hash = generateRandomHash();
@@ -26,7 +26,7 @@ networks:
`; `;
test("Add suffix to service names in compose file", () => { test("Add suffix to service names in compose file", () => {
const composeData = load(composeFile) as ComposeSpecification; const composeData = parse(composeFile) as ComposeSpecification;
const suffix = generateRandomHash(); const suffix = generateRandomHash();

View File

@@ -3,8 +3,8 @@ import {
addSuffixToAllServiceNames, addSuffixToAllServiceNames,
addSuffixToServiceNames, addSuffixToServiceNames,
} from "@dokploy/server"; } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";
import { parse } from "yaml";
const composeFileCombinedAllCases = ` const composeFileCombinedAllCases = `
version: "3.8" version: "3.8"
@@ -38,7 +38,7 @@ networks:
driver: bridge driver: bridge
`; `;
const expectedComposeFile = load(` const expectedComposeFile = parse(`
version: "3.8" version: "3.8"
services: services:
@@ -71,7 +71,9 @@ networks:
`); `);
test("Add suffix to all service names in compose file", () => { test("Add suffix to all service names in compose file", () => {
const composeData = load(composeFileCombinedAllCases) as ComposeSpecification; const composeData = parse(
composeFileCombinedAllCases,
) as ComposeSpecification;
const suffix = "testhash"; const suffix = "testhash";
@@ -131,7 +133,7 @@ networks:
driver: bridge driver: bridge
`; `;
const expectedComposeFile1 = load(` const expectedComposeFile1 = parse(`
version: "3.8" version: "3.8"
services: services:
@@ -176,7 +178,7 @@ networks:
`) as ComposeSpecification; `) as ComposeSpecification;
test("Add suffix to all service names in compose file 1", () => { test("Add suffix to all service names in compose file 1", () => {
const composeData = load(composeFile1) as ComposeSpecification; const composeData = parse(composeFile1) as ComposeSpecification;
const suffix = "testhash"; const suffix = "testhash";
const updatedComposeData = addSuffixToAllServiceNames(composeData, suffix); const updatedComposeData = addSuffixToAllServiceNames(composeData, suffix);
@@ -227,7 +229,7 @@ networks:
driver: bridge driver: bridge
`; `;
const expectedComposeFile2 = load(` const expectedComposeFile2 = parse(`
version: "3.8" version: "3.8"
services: services:
@@ -271,7 +273,7 @@ networks:
`) as ComposeSpecification; `) as ComposeSpecification;
test("Add suffix to all service names in compose file 2", () => { test("Add suffix to all service names in compose file 2", () => {
const composeData = load(composeFile2) as ComposeSpecification; const composeData = parse(composeFile2) as ComposeSpecification;
const suffix = "testhash"; const suffix = "testhash";
const updatedComposeData = addSuffixToAllServiceNames(composeData, suffix); const updatedComposeData = addSuffixToAllServiceNames(composeData, suffix);
@@ -322,7 +324,7 @@ networks:
driver: bridge driver: bridge
`; `;
const expectedComposeFile3 = load(` const expectedComposeFile3 = parse(`
version: "3.8" version: "3.8"
services: services:
@@ -366,7 +368,7 @@ networks:
`) as ComposeSpecification; `) as ComposeSpecification;
test("Add suffix to all service names in compose file 3", () => { test("Add suffix to all service names in compose file 3", () => {
const composeData = load(composeFile3) as ComposeSpecification; const composeData = parse(composeFile3) as ComposeSpecification;
const suffix = "testhash"; const suffix = "testhash";
const updatedComposeData = addSuffixToAllServiceNames(composeData, suffix); const updatedComposeData = addSuffixToAllServiceNames(composeData, suffix);

View File

@@ -1,7 +1,7 @@
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server"; import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";
import { parse } from "yaml";
test("Generate random hash with 8 characters", () => { test("Generate random hash with 8 characters", () => {
const hash = generateRandomHash(); const hash = generateRandomHash();
@@ -35,7 +35,7 @@ networks:
`; `;
test("Add suffix to service names with volumes_from in compose file", () => { test("Add suffix to service names with volumes_from in compose file", () => {
const composeData = load(composeFile3) as ComposeSpecification; const composeData = parse(composeFile3) as ComposeSpecification;
const suffix = generateRandomHash(); const suffix = generateRandomHash();

View File

@@ -4,8 +4,8 @@ import {
addSuffixToVolumesRoot, addSuffixToVolumesRoot,
generateRandomHash, generateRandomHash,
} from "@dokploy/server"; } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";
import { parse } from "yaml";
const composeFile = ` const composeFile = `
services: services:
@@ -70,7 +70,7 @@ volumes:
driver: local driver: local
`; `;
const expectedDockerCompose = load(` const expectedDockerCompose = parse(`
services: services:
mail: mail:
image: bytemark/smtp image: bytemark/smtp
@@ -143,7 +143,7 @@ test("Generate random hash with 8 characters", () => {
// Docker compose needs unique names for services, volumes, networks and containers // Docker compose needs unique names for services, volumes, networks and containers
// So base on a input which is a dockercompose file, it should replace the name with a hash and return a new dockercompose file // So base on a input which is a dockercompose file, it should replace the name with a hash and return a new dockercompose file
test("Add suffix to volumes root property", () => { test("Add suffix to volumes root property", () => {
const composeData = load(composeFile) as ComposeSpecification; const composeData = parse(composeFile) as ComposeSpecification;
const suffix = generateRandomHash(); const suffix = generateRandomHash();
@@ -165,7 +165,7 @@ test("Add suffix to volumes root property", () => {
}); });
test("Expect to change the suffix in all the possible places", () => { test("Expect to change the suffix in all the possible places", () => {
const composeData = load(composeFile) as ComposeSpecification; const composeData = parse(composeFile) as ComposeSpecification;
const suffix = "testhash"; const suffix = "testhash";
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix); const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
@@ -195,7 +195,7 @@ volumes:
mongo-data: mongo-data:
`; `;
const expectedDockerCompose2 = load(` const expectedDockerCompose2 = parse(`
version: '3.8' version: '3.8'
services: services:
app: app:
@@ -218,7 +218,7 @@ volumes:
`) as ComposeSpecification; `) as ComposeSpecification;
test("Expect to change the suffix in all the possible places (2 Try)", () => { test("Expect to change the suffix in all the possible places (2 Try)", () => {
const composeData = load(composeFile2) as ComposeSpecification; const composeData = parse(composeFile2) as ComposeSpecification;
const suffix = "testhash"; const suffix = "testhash";
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix); const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
@@ -248,7 +248,7 @@ volumes:
mongo-data: mongo-data:
`; `;
const expectedDockerCompose3 = load(` const expectedDockerCompose3 = parse(`
version: '3.8' version: '3.8'
services: services:
app: app:
@@ -271,7 +271,7 @@ volumes:
`) as ComposeSpecification; `) as ComposeSpecification;
test("Expect to change the suffix in all the possible places (3 Try)", () => { test("Expect to change the suffix in all the possible places (3 Try)", () => {
const composeData = load(composeFile3) as ComposeSpecification; const composeData = parse(composeFile3) as ComposeSpecification;
const suffix = "testhash"; const suffix = "testhash";
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix); const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
@@ -645,7 +645,7 @@ volumes:
db-config: db-config:
`; `;
const expectedDockerComposeComplex = load(` const expectedDockerComposeComplex = parse(`
version: "3.8" version: "3.8"
services: services:
studio: studio:
@@ -1012,7 +1012,7 @@ volumes:
`); `);
test("Expect to change the suffix in all the possible places (4 Try)", () => { test("Expect to change the suffix in all the possible places (4 Try)", () => {
const composeData = load(composeFileComplex) as ComposeSpecification; const composeData = parse(composeFileComplex) as ComposeSpecification;
const suffix = "testhash"; const suffix = "testhash";
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix); const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
@@ -1065,7 +1065,7 @@ volumes:
db-data: db-data:
`; `;
const expectedDockerComposeExample1 = load(` const expectedDockerComposeExample1 = parse(`
version: "3.8" version: "3.8"
services: services:
web: web:
@@ -1111,7 +1111,7 @@ volumes:
`) as ComposeSpecification; `) as ComposeSpecification;
test("Expect to change the suffix in all the possible places (5 Try)", () => { test("Expect to change the suffix in all the possible places (5 Try)", () => {
const composeData = load(composeFileExample1) as ComposeSpecification; const composeData = parse(composeFileExample1) as ComposeSpecification;
const suffix = "testhash"; const suffix = "testhash";
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix); const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
@@ -1143,7 +1143,7 @@ volumes:
backrest-cache: backrest-cache:
`; `;
const expectedDockerComposeBackrest = load(` const expectedDockerComposeBackrest = parse(`
services: services:
backrest: backrest:
image: garethgeorge/backrest:v1.7.3 image: garethgeorge/backrest:v1.7.3
@@ -1168,7 +1168,7 @@ volumes:
`) as ComposeSpecification; `) as ComposeSpecification;
test("Should handle volume paths with subdirectories correctly", () => { test("Should handle volume paths with subdirectories correctly", () => {
const composeData = load(composeFileBackrest) as ComposeSpecification; const composeData = parse(composeFileBackrest) as ComposeSpecification;
const suffix = "testhash"; const suffix = "testhash";
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix); const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);

View File

@@ -1,7 +1,7 @@
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToVolumesRoot, generateRandomHash } from "@dokploy/server"; import { addSuffixToVolumesRoot, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";
import { parse } from "yaml";
const composeFile = ` const composeFile = `
version: "3.8" version: "3.8"
@@ -29,7 +29,7 @@ test("Generate random hash with 8 characters", () => {
}); });
test("Add suffix to volumes in root property", () => { test("Add suffix to volumes in root property", () => {
const composeData = load(composeFile) as ComposeSpecification; const composeData = parse(composeFile) as ComposeSpecification;
const suffix = generateRandomHash(); const suffix = generateRandomHash();
@@ -67,7 +67,7 @@ networks:
`; `;
test("Add suffix to volumes in root property (Case 2)", () => { test("Add suffix to volumes in root property (Case 2)", () => {
const composeData = load(composeFile2) as ComposeSpecification; const composeData = parse(composeFile2) as ComposeSpecification;
const suffix = generateRandomHash(); const suffix = generateRandomHash();
@@ -101,7 +101,7 @@ networks:
`; `;
test("Add suffix to volumes in root property (Case 3)", () => { test("Add suffix to volumes in root property (Case 3)", () => {
const composeData = load(composeFile3) as ComposeSpecification; const composeData = parse(composeFile3) as ComposeSpecification;
const suffix = generateRandomHash(); const suffix = generateRandomHash();
@@ -148,7 +148,7 @@ volumes:
`; `;
// Expected compose file con el prefijo `testhash` // Expected compose file con el prefijo `testhash`
const expectedComposeFile4 = load(` const expectedComposeFile4 = parse(`
version: "3.8" version: "3.8"
services: services:
@@ -179,7 +179,7 @@ volumes:
`) as ComposeSpecification; `) as ComposeSpecification;
test("Add suffix to volumes in root property", () => { test("Add suffix to volumes in root property", () => {
const composeData = load(composeFile4) as ComposeSpecification; const composeData = parse(composeFile4) as ComposeSpecification;
const suffix = "testhash"; const suffix = "testhash";

View File

@@ -3,8 +3,8 @@ import {
addSuffixToVolumesInServices, addSuffixToVolumesInServices,
generateRandomHash, generateRandomHash,
} from "@dokploy/server"; } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";
import { parse } from "yaml";
test("Generate random hash with 8 characters", () => { test("Generate random hash with 8 characters", () => {
const hash = generateRandomHash(); const hash = generateRandomHash();
@@ -24,7 +24,7 @@ services:
`; `;
test("Add suffix to volumes declared directly in services", () => { test("Add suffix to volumes declared directly in services", () => {
const composeData = load(composeFile1) as ComposeSpecification; const composeData = parse(composeFile1) as ComposeSpecification;
const suffix = generateRandomHash(); const suffix = generateRandomHash();
@@ -59,7 +59,7 @@ volumes:
`; `;
test("Add suffix to volumes declared directly in services (Case 2)", () => { test("Add suffix to volumes declared directly in services (Case 2)", () => {
const composeData = load(composeFileTypeVolume) as ComposeSpecification; const composeData = parse(composeFileTypeVolume) as ComposeSpecification;
const suffix = generateRandomHash(); const suffix = generateRandomHash();

View File

@@ -1,7 +1,7 @@
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToAllVolumes } from "@dokploy/server"; import { addSuffixToAllVolumes } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";
import { parse } from "yaml";
const composeFileTypeVolume = ` const composeFileTypeVolume = `
version: "3.8" version: "3.8"
@@ -23,7 +23,7 @@ volumes:
driver: local driver: local
`; `;
const expectedComposeFileTypeVolume = load(` const expectedComposeFileTypeVolume = parse(`
version: "3.8" version: "3.8"
services: services:
@@ -44,7 +44,7 @@ volumes:
`) as ComposeSpecification; `) as ComposeSpecification;
test("Add suffix to volumes with type: volume in services", () => { test("Add suffix to volumes with type: volume in services", () => {
const composeData = load(composeFileTypeVolume) as ComposeSpecification; const composeData = parse(composeFileTypeVolume) as ComposeSpecification;
const suffix = "testhash"; const suffix = "testhash";
@@ -73,7 +73,7 @@ volumes:
driver: local driver: local
`; `;
const expectedComposeFileTypeVolume1 = load(` const expectedComposeFileTypeVolume1 = parse(`
version: "3.8" version: "3.8"
services: services:
@@ -93,7 +93,7 @@ volumes:
`) as ComposeSpecification; `) as ComposeSpecification;
test("Add suffix to mixed volumes in services", () => { test("Add suffix to mixed volumes in services", () => {
const composeData = load(composeFileTypeVolume1) as ComposeSpecification; const composeData = parse(composeFileTypeVolume1) as ComposeSpecification;
const suffix = "testhash"; const suffix = "testhash";
@@ -128,7 +128,7 @@ volumes:
device: /path/to/app/logs device: /path/to/app/logs
`; `;
const expectedComposeFileTypeVolume2 = load(` const expectedComposeFileTypeVolume2 = parse(`
version: "3.8" version: "3.8"
services: services:
@@ -154,7 +154,7 @@ volumes:
`) as ComposeSpecification; `) as ComposeSpecification;
test("Add suffix to complex volume configurations in services", () => { test("Add suffix to complex volume configurations in services", () => {
const composeData = load(composeFileTypeVolume2) as ComposeSpecification; const composeData = parse(composeFileTypeVolume2) as ComposeSpecification;
const suffix = "testhash"; const suffix = "testhash";
@@ -218,7 +218,7 @@ volumes:
device: /path/to/shared/logs device: /path/to/shared/logs
`; `;
const expectedComposeFileTypeVolume3 = load(` const expectedComposeFileTypeVolume3 = parse(`
version: "3.8" version: "3.8"
services: services:
@@ -273,7 +273,7 @@ volumes:
`) as ComposeSpecification; `) as ComposeSpecification;
test("Add suffix to complex nested volumes configuration in services", () => { test("Add suffix to complex nested volumes configuration in services", () => {
const composeData = load(composeFileTypeVolume3) as ComposeSpecification; const composeData = parse(composeFileTypeVolume3) as ComposeSpecification;
const suffix = "testhash"; const suffix = "testhash";

View File

@@ -133,6 +133,7 @@ const baseApp: ApplicationNested = {
username: null, username: null,
dockerContextPath: null, dockerContextPath: null,
rollbackActive: false, rollbackActive: false,
stopGracePeriodSwarm: null,
}; };
describe("unzipDrop using real zip files", () => { describe("unzipDrop using real zip files", () => {

View File

@@ -0,0 +1,102 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ApplicationNested } from "@dokploy/server/utils/builders";
import { mechanizeDockerContainer } from "@dokploy/server/utils/builders";
type MockCreateServiceOptions = {
StopGracePeriod?: number;
[key: string]: unknown;
};
const { inspectMock, getServiceMock, createServiceMock, getRemoteDockerMock } =
vi.hoisted(() => {
const inspect = vi.fn<[], Promise<never>>();
const getService = vi.fn(() => ({ inspect }));
const createService = vi.fn<[MockCreateServiceOptions], Promise<void>>(
async () => undefined,
);
const getRemoteDocker = vi.fn(async () => ({
getService,
createService,
}));
return {
inspectMock: inspect,
getServiceMock: getService,
createServiceMock: createService,
getRemoteDockerMock: getRemoteDocker,
};
});
vi.mock("@dokploy/server/utils/servers/remote-docker", () => ({
getRemoteDocker: getRemoteDockerMock,
}));
const createApplication = (
overrides: Partial<ApplicationNested> = {},
): ApplicationNested =>
({
appName: "test-app",
buildType: "dockerfile",
env: null,
mounts: [],
cpuLimit: null,
memoryLimit: null,
memoryReservation: null,
cpuReservation: null,
command: null,
ports: [],
sourceType: "docker",
dockerImage: "example:latest",
registry: null,
environment: {
project: { env: null },
env: null,
},
replicas: 1,
stopGracePeriodSwarm: 0n,
serverId: "server-id",
...overrides,
}) as unknown as ApplicationNested;
describe("mechanizeDockerContainer", () => {
beforeEach(() => {
inspectMock.mockReset();
inspectMock.mockRejectedValue(new Error("service not found"));
getServiceMock.mockClear();
createServiceMock.mockClear();
getRemoteDockerMock.mockClear();
getRemoteDockerMock.mockResolvedValue({
getService: getServiceMock,
createService: createServiceMock,
});
});
it("converts bigint stopGracePeriodSwarm to a number and keeps zero values", async () => {
const application = createApplication({ stopGracePeriodSwarm: 0n });
await mechanizeDockerContainer(application);
expect(createServiceMock).toHaveBeenCalledTimes(1);
const call = createServiceMock.mock.calls[0];
if (!call) {
throw new Error("createServiceMock should have been called once");
}
const [settings] = call;
expect(settings.StopGracePeriod).toBe(0);
expect(typeof settings.StopGracePeriod).toBe("number");
});
it("omits StopGracePeriod when stopGracePeriodSwarm is null", async () => {
const application = createApplication({ stopGracePeriodSwarm: null });
await mechanizeDockerContainer(application);
expect(createServiceMock).toHaveBeenCalledTimes(1);
const call = createServiceMock.mock.calls[0];
if (!call) {
throw new Error("createServiceMock should have been called once");
}
const [settings] = call;
expect(settings).not.toHaveProperty("StopGracePeriod");
});
});

View File

@@ -111,6 +111,7 @@ const baseApp: ApplicationNested = {
updateConfigSwarm: null, updateConfigSwarm: null,
username: null, username: null,
dockerContextPath: null, dockerContextPath: null,
stopGracePeriodSwarm: null,
}; };
const baseDomain: Domain = { const baseDomain: Domain = {

View File

@@ -25,6 +25,7 @@ import {
FormLabel, FormLabel,
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@@ -176,10 +177,18 @@ const addSwarmSettings = z.object({
modeSwarm: createStringToJSONSchema(ServiceModeSwarmSchema).nullable(), modeSwarm: createStringToJSONSchema(ServiceModeSwarmSchema).nullable(),
labelsSwarm: createStringToJSONSchema(LabelsSwarmSchema).nullable(), labelsSwarm: createStringToJSONSchema(LabelsSwarmSchema).nullable(),
networkSwarm: createStringToJSONSchema(NetworkSwarmSchema).nullable(), networkSwarm: createStringToJSONSchema(NetworkSwarmSchema).nullable(),
stopGracePeriodSwarm: z.bigint().nullable(),
}); });
type AddSwarmSettings = z.infer<typeof addSwarmSettings>; type AddSwarmSettings = z.infer<typeof addSwarmSettings>;
const hasStopGracePeriodSwarm = (
value: unknown,
): value is { stopGracePeriodSwarm: bigint | number | string | null } =>
typeof value === "object" &&
value !== null &&
"stopGracePeriodSwarm" in value;
interface Props { interface Props {
id: string; id: string;
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application"; type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
@@ -224,12 +233,22 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
modeSwarm: null, modeSwarm: null,
labelsSwarm: null, labelsSwarm: null,
networkSwarm: null, networkSwarm: null,
stopGracePeriodSwarm: null,
}, },
resolver: zodResolver(addSwarmSettings), resolver: zodResolver(addSwarmSettings),
}); });
useEffect(() => { useEffect(() => {
if (data) { if (data) {
const stopGracePeriodValue = hasStopGracePeriodSwarm(data)
? data.stopGracePeriodSwarm
: null;
const normalizedStopGracePeriod =
stopGracePeriodValue === null || stopGracePeriodValue === undefined
? null
: typeof stopGracePeriodValue === "bigint"
? stopGracePeriodValue
: BigInt(stopGracePeriodValue);
form.reset({ form.reset({
healthCheckSwarm: data.healthCheckSwarm healthCheckSwarm: data.healthCheckSwarm
? JSON.stringify(data.healthCheckSwarm, null, 2) ? JSON.stringify(data.healthCheckSwarm, null, 2)
@@ -255,6 +274,7 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
networkSwarm: data.networkSwarm networkSwarm: data.networkSwarm
? JSON.stringify(data.networkSwarm, null, 2) ? JSON.stringify(data.networkSwarm, null, 2)
: null, : null,
stopGracePeriodSwarm: normalizedStopGracePeriod,
}); });
} }
}, [form, form.reset, data]); }, [form, form.reset, data]);
@@ -275,6 +295,7 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
modeSwarm: data.modeSwarm, modeSwarm: data.modeSwarm,
labelsSwarm: data.labelsSwarm, labelsSwarm: data.labelsSwarm,
networkSwarm: data.networkSwarm, networkSwarm: data.networkSwarm,
stopGracePeriodSwarm: data.stopGracePeriodSwarm ?? null,
}) })
.then(async () => { .then(async () => {
toast.success("Swarm settings updated"); toast.success("Swarm settings updated");
@@ -352,9 +373,9 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
language="json" language="json"
placeholder={`{ placeholder={`{
"Test" : ["CMD-SHELL", "curl -f http://localhost:3000/health"], "Test" : ["CMD-SHELL", "curl -f http://localhost:3000/health"],
"Interval" : 10000, "Interval" : 10000000000,
"Timeout" : 10000, "Timeout" : 10000000000,
"StartPeriod" : 10000, "StartPeriod" : 10000000000,
"Retries" : 10 "Retries" : 10
}`} }`}
className="h-[12rem] font-mono" className="h-[12rem] font-mono"
@@ -407,9 +428,9 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
language="json" language="json"
placeholder={`{ placeholder={`{
"Condition" : "on-failure", "Condition" : "on-failure",
"Delay" : 10000, "Delay" : 10000000000,
"MaxAttempts" : 10, "MaxAttempts" : 10,
"Window" : 10000 "Window" : 10000000000
} `} } `}
className="h-[12rem] font-mono" className="h-[12rem] font-mono"
{...field} {...field}
@@ -529,9 +550,9 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
language="json" language="json"
placeholder={`{ placeholder={`{
"Parallelism" : 1, "Parallelism" : 1,
"Delay" : 10000, "Delay" : 10000000000,
"FailureAction" : "continue", "FailureAction" : "continue",
"Monitor" : 10000, "Monitor" : 10000000000,
"MaxFailureRatio" : 10, "MaxFailureRatio" : 10,
"Order" : "start-first" "Order" : "start-first"
}`} }`}
@@ -587,9 +608,9 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
language="json" language="json"
placeholder={`{ placeholder={`{
"Parallelism" : 1, "Parallelism" : 1,
"Delay" : 10000, "Delay" : 10000000000,
"FailureAction" : "continue", "FailureAction" : "continue",
"Monitor" : 10000, "Monitor" : 10000000000,
"MaxFailureRatio" : 10, "MaxFailureRatio" : 10,
"Order" : "start-first" "Order" : "start-first"
}`} }`}
@@ -774,7 +795,57 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="stopGracePeriodSwarm"
render={({ field }) => (
<FormItem className="relative max-lg:px-4 lg:pl-6 ">
<FormLabel>Stop Grace Period (nanoseconds)</FormLabel>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<FormDescription className="break-all w-fit flex flex-row gap-1 items-center">
Duration in nanoseconds
<HelpCircle className="size-4 text-muted-foreground" />
</FormDescription>
</TooltipTrigger>
<TooltipContent
className="w-full z-[999]"
align="start"
side="bottom"
>
<code>
<pre>
{`Enter duration in nanoseconds:
• 30000000000 - 30 seconds
• 120000000000 - 2 minutes
• 3600000000000 - 1 hour
• 0 - no grace period`}
</pre>
</code>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<FormControl>
<Input
type="number"
placeholder="30000000000"
className="font-mono"
{...field}
value={field?.value?.toString() || ""}
onChange={(e) =>
field.onChange(
e.target.value ? BigInt(e.target.value) : null,
)
}
/>
</FormControl>
<pre>
<FormMessage />
</pre>
</FormItem>
)}
/>
<DialogFooter className="flex w-full flex-row justify-end md:col-span-2 m-0 sticky bottom-0 right-0 bg-muted border"> <DialogFooter className="flex w-full flex-row justify-end md:col-span-2 m-0 sticky bottom-0 right-0 bg-muted border">
<Button <Button
isLoading={isLoading} isLoading={isLoading}

View File

@@ -1,8 +1,8 @@
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import jsyaml from "js-yaml";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { parse, stringify, YAMLParseError } from "yaml";
import { z } from "zod"; import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor"; import { CodeEditor } from "@/components/shared/code-editor";
@@ -38,11 +38,11 @@ interface Props {
export const validateAndFormatYAML = (yamlText: string) => { export const validateAndFormatYAML = (yamlText: string) => {
try { try {
const obj = jsyaml.load(yamlText); const obj = parse(yamlText);
const formattedYaml = jsyaml.dump(obj, { indent: 4 }); const formattedYaml = stringify(obj, { indent: 4 });
return { valid: true, formattedYaml, error: null }; return { valid: true, formattedYaml, error: null };
} catch (error) { } catch (error) {
if (error instanceof jsyaml.YAMLException) { if (error instanceof YAMLParseError) {
return { return {
valid: false, valid: false,
formattedYaml: yamlText, formattedYaml: yamlText,
@@ -89,7 +89,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
if (!valid) { if (!valid) {
form.setError("traefikConfig", { form.setError("traefikConfig", {
type: "manual", type: "manual",
message: error || "Invalid YAML", message: (error as string) || "Invalid YAML",
}); });
return; return;
} }

View File

@@ -59,7 +59,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
const router = useRouter(); const router = useRouter();
const { mutateAsync, isLoading } = const { mutateAsync, isLoading } =
api.application.saveGitProdiver.useMutation(); api.application.saveGitProvider.useMutation();
const form = useForm<GitProvider>({ const form = useForm<GitProvider>({
defaultValues: { defaultValues: {

View File

@@ -7,7 +7,7 @@ import {
RefreshCw, RefreshCw,
} from "lucide-react"; } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { type Control, useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
@@ -57,6 +57,7 @@ export const commonCronExpressions = [
{ label: "Every month on the 1st at midnight", value: "0 0 1 * *" }, { label: "Every month on the 1st at midnight", value: "0 0 1 * *" },
{ label: "Every 15 minutes", value: "*/15 * * * *" }, { label: "Every 15 minutes", value: "*/15 * * * *" },
{ label: "Every weekday at midnight", value: "0 0 * * 1-5" }, { label: "Every weekday at midnight", value: "0 0 * * 1-5" },
{ label: "Custom", value: "custom" },
]; ];
const formSchema = z const formSchema = z
@@ -115,10 +116,91 @@ interface Props {
scheduleType?: "application" | "compose" | "server" | "dokploy-server"; scheduleType?: "application" | "compose" | "server" | "dokploy-server";
} }
export const ScheduleFormField = ({
name,
formControl,
}: {
name: string;
formControl: Control<any>;
}) => {
const [selectedOption, setSelectedOption] = useState("");
return (
<FormField
control={formControl}
name={name}
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center gap-2">
Schedule
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
<p>Cron expression format: minute hour day month weekday</p>
<p>Example: 0 0 * * * (daily at midnight)</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</FormLabel>
<div className="flex flex-col gap-2">
<Select
value={selectedOption}
onValueChange={(value) => {
setSelectedOption(value);
field.onChange(value === "custom" ? "" : value);
}}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a predefined schedule" />
</SelectTrigger>
</FormControl>
<SelectContent>
{commonCronExpressions.map((expr) => (
<SelectItem key={expr.value} value={expr.value}>
{expr.label}
{expr.value !== "custom" && ` (${expr.value})`}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="relative">
<FormControl>
<Input
placeholder="Custom cron expression (e.g., 0 0 * * *)"
{...field}
onChange={(e) => {
const value = e.target.value;
const commonExpression = commonCronExpressions.find(
(expression) => expression.value === value,
);
if (commonExpression) {
setSelectedOption(commonExpression.value);
} else {
setSelectedOption("custom");
}
field.onChange(e);
}}
/>
</FormControl>
</div>
</div>
<FormDescription>
Choose a predefined schedule or enter a custom cron expression
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
);
};
export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => { export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [cacheType, setCacheType] = useState<CacheType>("cache"); const [cacheType, setCacheType] = useState<CacheType>("cache");
const utils = api.useUtils(); const utils = api.useUtils();
const form = useForm<z.infer<typeof formSchema>>({ const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
@@ -377,63 +459,9 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
)} )}
/> />
<FormField <ScheduleFormField
control={form.control}
name="cronExpression" name="cronExpression"
render={({ field }) => ( formControl={form.control}
<FormItem>
<FormLabel className="flex items-center gap-2">
Schedule
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
<p>
Cron expression format: minute hour day month
weekday
</p>
<p>Example: 0 0 * * * (daily at midnight)</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</FormLabel>
<div className="flex flex-col gap-2">
<Select
onValueChange={(value) => {
field.onChange(value);
}}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a predefined schedule" />
</SelectTrigger>
</FormControl>
<SelectContent>
{commonCronExpressions.map((expr) => (
<SelectItem key={expr.value} value={expr.value}>
{expr.label} ({expr.value})
</SelectItem>
))}
</SelectContent>
</Select>
<div className="relative">
<FormControl>
<Input
placeholder="Custom cron expression (e.g., 0 0 * * *)"
{...field}
/>
</FormControl>
</div>
</div>
<FormDescription>
Choose a predefined schedule or enter a custom cron
expression
</FormDescription>
<FormMessage />
</FormItem>
)}
/> />
{(scheduleTypeForm === "application" || {(scheduleTypeForm === "application" ||

View File

@@ -1,11 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { import { DatabaseZap, PenBoxIcon, PlusCircle, RefreshCw } from "lucide-react";
DatabaseZap,
Info,
PenBoxIcon,
PlusCircle,
RefreshCw,
} from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -47,7 +41,7 @@ import {
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import type { CacheType } from "../domains/handle-domain"; import type { CacheType } from "../domains/handle-domain";
import { commonCronExpressions } from "../schedules/handle-schedules"; import { ScheduleFormField } from "../schedules/handle-schedules";
const formSchema = z const formSchema = z
.object({ .object({
@@ -306,64 +300,9 @@ export const HandleVolumeBackups = ({
</FormItem> </FormItem>
)} )}
/> />
<ScheduleFormField
<FormField
control={form.control}
name="cronExpression" name="cronExpression"
render={({ field }) => ( formControl={form.control}
<FormItem>
<FormLabel className="flex items-center gap-2">
Schedule
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
<p>
Cron expression format: minute hour day month
weekday
</p>
<p>Example: 0 0 * * * (daily at midnight)</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</FormLabel>
<div className="flex flex-col gap-2">
<Select
onValueChange={(value) => {
field.onChange(value);
}}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a predefined schedule" />
</SelectTrigger>
</FormControl>
<SelectContent>
{commonCronExpressions.map((expr) => (
<SelectItem key={expr.value} value={expr.value}>
{expr.label} ({expr.value})
</SelectItem>
))}
</SelectContent>
</Select>
<div className="relative">
<FormControl>
<Input
placeholder="Custom cron expression (e.g., 0 0 * * *)"
{...field}
/>
</FormControl>
</div>
</div>
<FormDescription>
Choose a predefined schedule or enter a custom cron
expression
</FormDescription>
<FormMessage />
</FormItem>
)}
/> />
<FormField <FormField

View File

@@ -1,5 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect } from "react"; import { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
@@ -35,6 +35,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
); );
const { mutateAsync, isLoading } = api.compose.update.useMutation(); const { mutateAsync, isLoading } = api.compose.update.useMutation();
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const form = useForm<AddComposeFile>({ const form = useForm<AddComposeFile>({
defaultValues: { defaultValues: {
@@ -53,6 +54,12 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
} }
}, [form, form.reset, data]); }, [form, form.reset, data]);
useEffect(() => {
if (data?.composeFile !== undefined) {
setHasUnsavedChanges(composeFile !== data.composeFile);
}
}, [composeFile, data?.composeFile]);
const onSubmit = async (data: AddComposeFile) => { const onSubmit = async (data: AddComposeFile) => {
const { valid, error } = validateAndFormatYAML(data.composeFile); const { valid, error } = validateAndFormatYAML(data.composeFile);
if (!valid) { if (!valid) {
@@ -71,6 +78,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
}) })
.then(async () => { .then(async () => {
toast.success("Compose config Updated"); toast.success("Compose config Updated");
setHasUnsavedChanges(false);
refetch(); refetch();
await utils.compose.getConvertedCompose.invalidate({ await utils.compose.getConvertedCompose.invalidate({
composeId, composeId,
@@ -99,6 +107,19 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
return ( return (
<> <>
<div className="w-full flex flex-col gap-4 "> <div className="w-full flex flex-col gap-4 ">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-medium">Compose File</h3>
<p className="text-sm text-muted-foreground">
Configure your Docker Compose file for this service.
{hasUnsavedChanges && (
<span className="text-yellow-500 ml-2">
(You have unsaved changes)
</span>
)}
</p>
</div>
</div>
<Form {...form}> <Form {...form}>
<form <form
id="hook-form-save-compose-file" id="hook-form-save-compose-file"

View File

@@ -3,7 +3,6 @@ import {
CheckIcon, CheckIcon,
ChevronsUpDown, ChevronsUpDown,
DatabaseZap, DatabaseZap,
Info,
PenBoxIcon, PenBoxIcon,
PlusIcon, PlusIcon,
RefreshCw, RefreshCw,
@@ -62,7 +61,7 @@ import {
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { commonCronExpressions } from "../../application/schedules/handle-schedules"; import { ScheduleFormField } from "../../application/schedules/handle-schedules";
type CacheType = "cache" | "fetch"; type CacheType = "cache" | "fetch";
@@ -579,66 +578,9 @@ export const HandleBackup = ({
); );
}} }}
/> />
<FormField
control={form.control} <ScheduleFormField name="schedule" formControl={form.control} />
name="schedule"
render={({ field }) => {
return (
<FormItem>
<FormLabel className="flex items-center gap-2">
Schedule
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
<p>
Cron expression format: minute hour day month
weekday
</p>
<p>Example: 0 0 * * * (daily at midnight)</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</FormLabel>
<div className="flex flex-col gap-2">
<Select
onValueChange={(value) => {
field.onChange(value);
}}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a predefined schedule" />
</SelectTrigger>
</FormControl>
<SelectContent>
{commonCronExpressions.map((expr) => (
<SelectItem key={expr.value} value={expr.value}>
{expr.label} ({expr.value})
</SelectItem>
))}
</SelectContent>
</Select>
<div className="relative">
<FormControl>
<Input
placeholder="Custom cron expression (e.g., 0 0 * * *)"
{...field}
/>
</FormControl>
</div>
</div>
<FormDescription>
Choose a predefined schedule or enter a custom cron
expression
</FormDescription>
<FormMessage />
</FormItem>
);
}}
/>
<FormField <FormField
control={form.control} control={form.control}
name="prefix" name="prefix"

View File

@@ -171,7 +171,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
<Input <Input
placeholder="Search Template" placeholder="Search Template"
onChange={(e) => setQuery(e.target.value)} onChange={(e) => setQuery(e.target.value)}
className="w-full sm:w-[200px]" className="w-full"
value={query} value={query}
/> />
<Input <Input
@@ -248,7 +248,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
onClick={() => onClick={() =>
setViewMode(viewMode === "detailed" ? "icon" : "detailed") setViewMode(viewMode === "detailed" ? "icon" : "detailed")
} }
className="h-9 w-9" className="h-9 w-9 flex-shrink-0"
> >
{viewMode === "detailed" ? ( {viewMode === "detailed" ? (
<LayoutGrid className="size-4" /> <LayoutGrid className="size-4" />

View File

@@ -88,7 +88,7 @@ export const StepThree = ({ templateInfo }: StepProps) => {
<div> <div>
<h3 className="text-sm font-semibold">Configuration Files</h3> <h3 className="text-sm font-semibold">Configuration Files</h3>
<ul className="list-disc pl-5"> <ul className="list-disc pl-5">
{templateInfo?.details?.configFiles.map((file, index) => ( {templateInfo?.details?.configFiles?.map((file, index) => (
<li key={index}> <li key={index}>
<strong className="text-sm font-semibold"> <strong className="text-sm font-semibold">
{file.filePath} {file.filePath}

View File

@@ -1,5 +1,5 @@
import { Bot, Eye, EyeOff, PlusCircle, Trash2 } from "lucide-react"; import { Bot, PlusCircle, Trash2 } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect } from "react";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import { toast } from "sonner"; import { toast } from "sonner";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
@@ -27,7 +27,6 @@ export interface StepProps {
export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => { export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
const suggestions = templateInfo.suggestions || []; const suggestions = templateInfo.suggestions || [];
const selectedVariant = templateInfo.details; const selectedVariant = templateInfo.details;
const [showValues, setShowValues] = useState<Record<string, boolean>>({});
const { mutateAsync, isLoading, error, isError } = const { mutateAsync, isLoading, error, isError } =
api.ai.suggest.useMutation(); api.ai.suggest.useMutation();
@@ -44,7 +43,7 @@ export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
.then((data) => { .then((data) => {
setTemplateInfo({ setTemplateInfo({
...templateInfo, ...templateInfo,
suggestions: data, suggestions: data || [],
}); });
}) })
.catch((error) => { .catch((error) => {
@@ -54,10 +53,6 @@ export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
}); });
}, [templateInfo.userInput]); }, [templateInfo.userInput]);
const toggleShowValue = (name: string) => {
setShowValues((prev) => ({ ...prev, [name]: !prev[name] }));
};
const handleEnvVariableChange = ( const handleEnvVariableChange = (
index: number, index: number,
field: "name" | "value", field: "name" | "value",
@@ -308,11 +303,9 @@ export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
placeholder="Variable Name" placeholder="Variable Name"
className="flex-1" className="flex-1"
/> />
<div className="flex-1 relative"> <div className="relative">
<Input <Input
type={ type={"password"}
showValues[env.name] ? "text" : "password"
}
value={env.value} value={env.value}
onChange={(e) => onChange={(e) =>
handleEnvVariableChange( handleEnvVariableChange(
@@ -323,19 +316,6 @@ export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
} }
placeholder="Variable Value" placeholder="Variable Value"
/> />
<Button
type="button"
variant="ghost"
size="icon"
className="absolute right-2 top-1/2 transform -translate-y-1/2"
onClick={() => toggleShowValue(env.name)}
>
{showValues[env.name] ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</Button>
</div> </div>
<Button <Button
type="button" type="button"
@@ -437,13 +417,14 @@ export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
<AccordionContent> <AccordionContent>
<ScrollArea className="w-full rounded-md border"> <ScrollArea className="w-full rounded-md border">
<div className="p-4 space-y-4"> <div className="p-4 space-y-4">
{selectedVariant?.configFiles?.length > 0 ? ( {selectedVariant?.configFiles?.length &&
selectedVariant?.configFiles?.length > 0 ? (
<> <>
<div className="text-sm text-muted-foreground mb-4"> <div className="text-sm text-muted-foreground mb-4">
This template requires the following This template requires the following
configuration files to be mounted: configuration files to be mounted:
</div> </div>
{selectedVariant.configFiles.map( {selectedVariant?.configFiles?.map(
(config, index) => ( (config, index) => (
<div <div
key={index} key={index}

View File

@@ -47,7 +47,7 @@ interface Details {
envVariables: EnvVariable[]; envVariables: EnvVariable[];
shortDescription: string; shortDescription: string;
domains: Domain[]; domains: Domain[];
configFiles: Mount[]; configFiles?: Mount[];
} }
interface Mount { interface Mount {

View File

@@ -80,6 +80,29 @@ export const DuplicateProject = ({
api.project.duplicate.useMutation({ api.project.duplicate.useMutation({
onSuccess: async (newProject) => { onSuccess: async (newProject) => {
await utils.project.all.invalidate(); await utils.project.all.invalidate();
// If duplicating to same project+environment, invalidate the environment query
// to refresh the services list
if (duplicateType === "existing-environment") {
await utils.environment.one.invalidate({
environmentId: selectedTargetEnvironment,
});
await utils.environment.byProjectId.invalidate({
projectId: selectedTargetProject,
});
// If duplicating to the same environment we're currently viewing,
// also invalidate the current environment to refresh the services list
if (selectedTargetEnvironment === environmentId) {
await utils.environment.one.invalidate({ environmentId });
// Also invalidate the project query to refresh the project data
const projectId = router.query.projectId as string;
if (projectId) {
await utils.project.one.invalidate({ projectId });
}
}
}
toast.success( toast.success(
duplicateType === "new-project" duplicateType === "new-project"
? "Project duplicated successfully" ? "Project duplicated successfully"

View File

@@ -96,8 +96,30 @@ export const ShowProjects = () => {
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
break; break;
case "services": { case "services": {
const aTotalServices = a.environments.length; const aTotalServices = a.environments.reduce((total, env) => {
const bTotalServices = b.environments.length; return (
total +
(env.applications?.length || 0) +
(env.mariadb?.length || 0) +
(env.mongo?.length || 0) +
(env.mysql?.length || 0) +
(env.postgres?.length || 0) +
(env.redis?.length || 0) +
(env.compose?.length || 0)
);
}, 0);
const bTotalServices = b.environments.reduce((total, env) => {
return (
total +
(env.applications?.length || 0) +
(env.mariadb?.length || 0) +
(env.mongo?.length || 0) +
(env.mysql?.length || 0) +
(env.postgres?.length || 0) +
(env.redis?.length || 0) +
(env.compose?.length || 0)
);
}, 0);
comparison = aTotalServices - bTotalServices; comparison = aTotalServices - bTotalServices;
break; break;
} }
@@ -291,45 +313,48 @@ export const ShowProjects = () => {
)} )}
</DropdownMenuGroup> </DropdownMenuGroup>
)} )}
{/* {project.environments.some(
{project.compose.length > 0 && ( (env) => env.compose.length > 0,
) && (
<DropdownMenuGroup> <DropdownMenuGroup>
<DropdownMenuLabel> <DropdownMenuLabel>
Compose Compose
</DropdownMenuLabel> </DropdownMenuLabel>
{project.compose.map((comp) => ( {project.environments.map((env) =>
<div key={comp.composeId}> env.compose.map((comp) => (
<DropdownMenuSeparator /> <div key={comp.composeId}>
<DropdownMenuGroup>
<DropdownMenuLabel className="font-normal capitalize text-xs flex items-center justify-between">
{comp.name}
<StatusTooltip
status={comp.composeStatus}
/>
</DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
{comp.domains.map((domain) => ( <DropdownMenuGroup>
<DropdownMenuItem <DropdownMenuLabel className="font-normal capitalize text-xs flex items-center justify-between">
key={domain.domainId} {comp.name}
asChild <StatusTooltip
> status={comp.composeStatus}
<Link />
className="space-x-4 text-xs cursor-pointer justify-between" </DropdownMenuLabel>
target="_blank" <DropdownMenuSeparator />
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`} {comp.domains.map((domain) => (
<DropdownMenuItem
key={domain.domainId}
asChild
> >
<span className="truncate"> <Link
{domain.host} className="space-x-4 text-xs cursor-pointer justify-between"
</span> target="_blank"
<ExternalLinkIcon className="size-4 shrink-0" /> href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
</Link> >
</DropdownMenuItem> <span className="truncate">
))} {domain.host}
</DropdownMenuGroup> </span>
</div> <ExternalLinkIcon className="size-4 shrink-0" />
))} </Link>
</DropdownMenuItem>
))}
</DropdownMenuGroup>
</div>
)),
)}
</DropdownMenuGroup> </DropdownMenuGroup>
)} */} )}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
) : null} ) : null}

View File

@@ -1,7 +1,6 @@
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { ExternalLink } from "lucide-react"; import { ExternalLink } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -27,18 +26,12 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { useUrl } from "@/utils/hooks/use-url";
const Schema = z.object({ const Schema = z.object({
name: z.string().min(1, { name: z.string().min(1, { message: "Name is required" }),
message: "Name is required", username: z.string().min(1, { message: "Username is required" }),
}), email: z.string().email().optional(),
username: z.string().min(1, { apiToken: z.string().min(1, { message: "API Token is required" }),
message: "Username is required",
}),
password: z.string().min(1, {
message: "App Password is required",
}),
workspaceName: z.string().optional(), workspaceName: z.string().optional(),
}); });
@@ -47,14 +40,12 @@ type Schema = z.infer<typeof Schema>;
export const AddBitbucketProvider = () => { export const AddBitbucketProvider = () => {
const utils = api.useUtils(); const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const _url = useUrl();
const { mutateAsync, error, isError } = api.bitbucket.create.useMutation(); const { mutateAsync, error, isError } = api.bitbucket.create.useMutation();
const { data: auth } = api.user.get.useQuery(); const { data: auth } = api.user.get.useQuery();
const _router = useRouter();
const form = useForm<Schema>({ const form = useForm<Schema>({
defaultValues: { defaultValues: {
username: "", username: "",
password: "", apiToken: "",
workspaceName: "", workspaceName: "",
}, },
resolver: zodResolver(Schema), resolver: zodResolver(Schema),
@@ -63,7 +54,8 @@ export const AddBitbucketProvider = () => {
useEffect(() => { useEffect(() => {
form.reset({ form.reset({
username: "", username: "",
password: "", email: "",
apiToken: "",
workspaceName: "", workspaceName: "",
}); });
}, [form, isOpen]); }, [form, isOpen]);
@@ -71,10 +63,11 @@ export const AddBitbucketProvider = () => {
const onSubmit = async (data: Schema) => { const onSubmit = async (data: Schema) => {
await mutateAsync({ await mutateAsync({
bitbucketUsername: data.username, bitbucketUsername: data.username,
appPassword: data.password, apiToken: data.apiToken,
bitbucketWorkspaceName: data.workspaceName || "", bitbucketWorkspaceName: data.workspaceName || "",
authId: auth?.id || "", authId: auth?.id || "",
name: data.name || "", name: data.name || "",
bitbucketEmail: data.email || "",
}) })
.then(async () => { .then(async () => {
await utils.gitProvider.getAll.invalidate(); await utils.gitProvider.getAll.invalidate();
@@ -113,37 +106,46 @@ export const AddBitbucketProvider = () => {
> >
<CardContent className="p-0"> <CardContent className="p-0">
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<AlertBlock type="warning">
Bitbucket App Passwords are deprecated for new providers. Use
an API Token instead. Existing providers with App Passwords
will continue to work until 9th June 2026.
</AlertBlock>
<div className="mt-1 text-sm">
Manage tokens in
<Link
href="https://id.atlassian.com/manage-profile/security/api-tokens"
target="_blank"
className="inline-flex items-center gap-1 ml-1"
>
<span>Bitbucket settings</span>
<ExternalLink className="w-fit text-primary size-4" />
</Link>
</div>
<ul className="list-disc list-inside ml-4 text-sm text-muted-foreground">
<li className="text-muted-foreground text-sm">
Click on Create API token with scopes
</li>
<li className="text-muted-foreground text-sm">
Select the expiration date (Max 1 year)
</li>
<li className="text-muted-foreground text-sm">
Select Bitbucket product.
</li>
</ul>
<p className="text-muted-foreground text-sm"> <p className="text-muted-foreground text-sm">
To integrate your Bitbucket account, you need to create a new Select the following scopes:
App Password in your Bitbucket settings. Follow these steps:
</p> </p>
<ol className="list-decimal list-inside text-sm text-muted-foreground">
<li className="flex flex-row gap-2 items-center"> <ul className="list-disc list-inside ml-4 text-sm text-muted-foreground">
Create new App Password{" "} <li>read:repository:bitbucket</li>
<Link <li>read:pullrequest:bitbucket</li>
href="https://bitbucket.org/account/settings/app-passwords/new" <li>read:webhook:bitbucket</li>
target="_blank" <li>read:workspace:bitbucket</li>
> <li>write:webhook:bitbucket</li>
<ExternalLink className="w-fit text-primary size-4" /> </ul>
</Link>
</li>
<li>
When creating the App Password, ensure you grant the
following permissions:
<ul className="list-disc list-inside ml-4">
<li>Account: Read</li>
<li>Workspace membership: Read</li>
<li>Projects: Read</li>
<li>Repositories: Read</li>
<li>Pull requests: Read</li>
<li>Webhooks: Read and write</li>
</ul>
</li>
<li>
After creating, you'll receive an App Password. Copy it and
paste it below along with your Bitbucket username.
</li>
</ol>
<FormField <FormField
control={form.control} control={form.control}
name="name" name="name"
@@ -152,7 +154,7 @@ export const AddBitbucketProvider = () => {
<FormLabel>Name</FormLabel> <FormLabel>Name</FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder="Random Name eg(my-personal-account)" placeholder="Your Bitbucket Provider, eg: my-personal-account"
{...field} {...field}
/> />
</FormControl> </FormControl>
@@ -179,14 +181,27 @@ export const AddBitbucketProvider = () => {
<FormField <FormField
control={form.control} control={form.control}
name="password" name="email"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>App Password</FormLabel> <FormLabel>Bitbucket Email</FormLabel>
<FormControl>
<Input placeholder="Your Bitbucket email" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="apiToken"
render={({ field }) => (
<FormItem>
<FormLabel>API Token</FormLabel>
<FormControl> <FormControl>
<Input <Input
type="password" placeholder="Paste your Bitbucket API token"
placeholder="ATBBPDYUC94nR96Nj7Cqpp4pfwKk03573DD2"
{...field} {...field}
/> />
</FormControl> </FormControl>
@@ -200,7 +215,7 @@ export const AddBitbucketProvider = () => {
name="workspaceName" name="workspaceName"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Workspace Name (Optional)</FormLabel> <FormLabel>Workspace Name (optional)</FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder="For organization accounts" placeholder="For organization accounts"

View File

@@ -33,7 +33,10 @@ const Schema = z.object({
username: z.string().min(1, { username: z.string().min(1, {
message: "Username is required", message: "Username is required",
}), }),
email: z.string().email().optional(),
workspaceName: z.string().optional(), workspaceName: z.string().optional(),
apiToken: z.string().optional(),
appPassword: z.string().optional(),
}); });
type Schema = z.infer<typeof Schema>; type Schema = z.infer<typeof Schema>;
@@ -60,19 +63,28 @@ export const EditBitbucketProvider = ({ bitbucketId }: Props) => {
const form = useForm<Schema>({ const form = useForm<Schema>({
defaultValues: { defaultValues: {
username: "", username: "",
email: "",
workspaceName: "", workspaceName: "",
apiToken: "",
appPassword: "",
}, },
resolver: zodResolver(Schema), resolver: zodResolver(Schema),
}); });
const username = form.watch("username"); const username = form.watch("username");
const email = form.watch("email");
const workspaceName = form.watch("workspaceName"); const workspaceName = form.watch("workspaceName");
const apiToken = form.watch("apiToken");
const appPassword = form.watch("appPassword");
useEffect(() => { useEffect(() => {
form.reset({ form.reset({
username: bitbucket?.bitbucketUsername || "", username: bitbucket?.bitbucketUsername || "",
email: bitbucket?.bitbucketEmail || "",
workspaceName: bitbucket?.bitbucketWorkspaceName || "", workspaceName: bitbucket?.bitbucketWorkspaceName || "",
name: bitbucket?.gitProvider.name || "", name: bitbucket?.gitProvider.name || "",
apiToken: bitbucket?.apiToken || "",
appPassword: bitbucket?.appPassword || "",
}); });
}, [form, isOpen, bitbucket]); }, [form, isOpen, bitbucket]);
@@ -81,8 +93,11 @@ export const EditBitbucketProvider = ({ bitbucketId }: Props) => {
bitbucketId, bitbucketId,
gitProviderId: bitbucket?.gitProviderId || "", gitProviderId: bitbucket?.gitProviderId || "",
bitbucketUsername: data.username, bitbucketUsername: data.username,
bitbucketEmail: data.email || "",
bitbucketWorkspaceName: data.workspaceName || "", bitbucketWorkspaceName: data.workspaceName || "",
name: data.name || "", name: data.name || "",
apiToken: data.apiToken || "",
appPassword: data.appPassword || "",
}) })
.then(async () => { .then(async () => {
await utils.gitProvider.getAll.invalidate(); await utils.gitProvider.getAll.invalidate();
@@ -121,6 +136,12 @@ export const EditBitbucketProvider = ({ bitbucketId }: Props) => {
> >
<CardContent className="p-0"> <CardContent className="p-0">
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<p className="text-muted-foreground text-sm">
Update your Bitbucket authentication. Use API Token for
enhanced security (recommended) or App Password for legacy
support.
</p>
<FormField <FormField
control={form.control} control={form.control}
name="name" name="name"
@@ -154,6 +175,24 @@ export const EditBitbucketProvider = ({ bitbucketId }: Props) => {
)} )}
/> />
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email (Required for API Tokens)</FormLabel>
<FormControl>
<Input
type="email"
placeholder="Your Bitbucket email address"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="workspaceName" name="workspaceName"
@@ -171,6 +210,49 @@ export const EditBitbucketProvider = ({ bitbucketId }: Props) => {
)} )}
/> />
<div className="flex flex-col gap-2 border-t pt-4">
<h3 className="text-sm font-medium mb-2">
Authentication (Update to use API Token)
</h3>
<FormField
control={form.control}
name="apiToken"
render={({ field }) => (
<FormItem>
<FormLabel>API Token (Recommended)</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Enter your Bitbucket API Token"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="appPassword"
render={({ field }) => (
<FormItem>
<FormLabel>
App Password (Legacy - will be deprecated June 2026)
</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Enter your Bitbucket App Password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex w-full justify-between gap-4 mt-4"> <div className="flex w-full justify-between gap-4 mt-4">
<Button <Button
type="button" type="button"
@@ -180,7 +262,10 @@ export const EditBitbucketProvider = ({ bitbucketId }: Props) => {
await testConnection({ await testConnection({
bitbucketId, bitbucketId,
bitbucketUsername: username, bitbucketUsername: username,
bitbucketEmail: email,
workspaceName: workspaceName, workspaceName: workspaceName,
apiToken: apiToken,
appPassword: appPassword,
}) })
.then(async (message) => { .then(async (message) => {
toast.info(`Message: ${message}`); toast.info(`Message: ${message}`);

View File

@@ -30,6 +30,9 @@ const Schema = z.object({
name: z.string().min(1, { name: z.string().min(1, {
message: "Name is required", message: "Name is required",
}), }),
appName: z.string().min(1, {
message: "App Name is required",
}),
}); });
type Schema = z.infer<typeof Schema>; type Schema = z.infer<typeof Schema>;
@@ -55,6 +58,7 @@ export const EditGithubProvider = ({ githubId }: Props) => {
const form = useForm<Schema>({ const form = useForm<Schema>({
defaultValues: { defaultValues: {
name: "", name: "",
appName: "",
}, },
resolver: zodResolver(Schema), resolver: zodResolver(Schema),
}); });
@@ -62,6 +66,7 @@ export const EditGithubProvider = ({ githubId }: Props) => {
useEffect(() => { useEffect(() => {
form.reset({ form.reset({
name: github?.gitProvider.name || "", name: github?.gitProvider.name || "",
appName: github?.githubAppName || "",
}); });
}, [form, isOpen]); }, [form, isOpen]);
@@ -70,6 +75,7 @@ export const EditGithubProvider = ({ githubId }: Props) => {
githubId, githubId,
name: data.name || "", name: data.name || "",
gitProviderId: github?.gitProviderId || "", gitProviderId: github?.gitProviderId || "",
githubAppName: data.appName || "",
}) })
.then(async () => { .then(async () => {
await utils.gitProvider.getAll.invalidate(); await utils.gitProvider.getAll.invalidate();
@@ -124,6 +130,22 @@ export const EditGithubProvider = ({ githubId }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="appName"
render={({ field }) => (
<FormItem>
<FormLabel>App Name</FormLabel>
<FormControl>
<Input
placeholder="pp Name eg(my-personal)"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex w-full justify-between gap-4 mt-4"> <div className="flex w-full justify-between gap-4 mt-4">
<Button <Button

View File

@@ -157,7 +157,13 @@ export const ShowGitProviders = () => {
</div> </div>
</div> </div>
<div className="flex flex-row gap-1"> <div className="flex flex-row gap-1 items-center">
{isBitbucket &&
gitProvider.bitbucket?.appPassword &&
!gitProvider.bitbucket?.apiToken ? (
<Badge variant="yellow">Deprecated</Badge>
) : null}
{!haveGithubRequirements && isGithub && ( {!haveGithubRequirements && isGithub && (
<div className="flex flex-row gap-1 items-center"> <div className="flex flex-row gap-1 items-center">
<Badge <Badge

View File

@@ -12,6 +12,8 @@ import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
import { import {
DiscordIcon, DiscordIcon,
GotifyIcon,
NtfyIcon,
SlackIcon, SlackIcon,
TelegramIcon, TelegramIcon,
} from "@/components/icons/notification-icons"; } from "@/components/icons/notification-icons";
@@ -130,11 +132,11 @@ export const notificationsMap = {
label: "Email", label: "Email",
}, },
gotify: { gotify: {
icon: <MessageCircleMore size={29} className="text-muted-foreground" />, icon: <GotifyIcon />,
label: "Gotify", label: "Gotify",
}, },
ntfy: { ntfy: {
icon: <MessageCircleMore size={29} className="text-muted-foreground" />, icon: <NtfyIcon />,
label: "ntfy", label: "ntfy",
}, },
}; };

View File

@@ -2,6 +2,8 @@ import { Bell, Loader2, Mail, MessageCircleMore, Trash2 } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { import {
DiscordIcon, DiscordIcon,
GotifyIcon,
NtfyIcon,
SlackIcon, SlackIcon,
TelegramIcon, TelegramIcon,
} from "@/components/icons/notification-icons"; } from "@/components/icons/notification-icons";
@@ -85,12 +87,12 @@ export const ShowNotifications = () => {
)} )}
{notification.notificationType === "gotify" && ( {notification.notificationType === "gotify" && (
<div className="flex items-center justify-center rounded-lg "> <div className="flex items-center justify-center rounded-lg ">
<MessageCircleMore className="size-6 text-muted-foreground" /> <GotifyIcon className="size-6" />
</div> </div>
)} )}
{notification.notificationType === "ntfy" && ( {notification.notificationType === "ntfy" && (
<div className="flex items-center justify-center rounded-lg "> <div className="flex items-center justify-center rounded-lg ">
<MessageCircleMore className="size-6 text-muted-foreground" /> <NtfyIcon className="size-6" />
</div> </div>
)} )}

View File

@@ -33,7 +33,10 @@ import { Disable2FA } from "./disable-2fa";
import { Enable2FA } from "./enable-2fa"; import { Enable2FA } from "./enable-2fa";
const profileSchema = z.object({ const profileSchema = z.object({
email: z.string(), email: z
.string()
.email("Please enter a valid email address")
.min(1, "Email is required"),
password: z.string().nullable(), password: z.string().nullable(),
currentPassword: z.string().nullable(), currentPassword: z.string().nullable(),
image: z.string().optional(), image: z.string().optional(),

View File

@@ -5,6 +5,7 @@ import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Card, Card,
@@ -76,6 +77,9 @@ export const WebDomain = () => {
resolver: zodResolver(addServerDomain), resolver: zodResolver(addServerDomain),
}); });
const https = form.watch("https"); const https = form.watch("https");
const domain = form.watch("domain") || "";
const host = data?.user?.host || "";
const hasChanged = domain !== host;
useEffect(() => { useEffect(() => {
if (data) { if (data) {
form.reset({ form.reset({
@@ -119,6 +123,19 @@ export const WebDomain = () => {
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="space-y-2 py-6 border-t"> <CardContent className="space-y-2 py-6 border-t">
{/* Warning for GitHub webhook URL changes */}
{hasChanged && (
<AlertBlock type="warning">
<div className="space-y-2">
<p className="font-medium"> Important: URL Change Impact</p>
<p>
If you change the Dokploy Server URL make sure to update
your Github Apps to keep the auto-deploy working and preview
deployments working.
</p>
</div>
</AlertBlock>
)}
<Form {...form}> <Form {...form}>
<form <form
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}

View File

@@ -88,3 +88,121 @@ export const DiscordIcon = ({ className }: Props) => {
</svg> </svg>
); );
}; };
export const GotifyIcon = ({ className }: Props) => {
return (
<svg
viewBox="0 0 500 500"
className={cn("size-8", className)}
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<style>
{`
.gotify-st0{fill:#DDCBA2;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-miterlimit:10;}
.gotify-st1{fill:#71CAEE;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-miterlimit:10;}
.gotify-st2{fill:#FFFFFF;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-miterlimit:10;}
.gotify-st3{fill:#888E93;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-miterlimit:10;}
.gotify-st4{fill:#F0F0F0;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-miterlimit:10;}
.gotify-st5{fill:none;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-miterlimit:10;}
.gotify-st8{fill:#FFFFFF;}
`}
</style>
<linearGradient
id="gotify-gradient"
x1="265"
y1="280"
x2="275"
y2="302"
gradientUnits="userSpaceOnUse"
>
<stop offset="0" stopColor="#71CAEE" />
<stop offset="0.04" stopColor="#83CAE2" />
<stop offset="0.12" stopColor="#9FCACE" />
<stop offset="0.21" stopColor="#B6CBBE" />
<stop offset="0.31" stopColor="#C7CBB1" />
<stop offset="0.44" stopColor="#D4CBA8" />
<stop offset="0.61" stopColor="#DBCBA3" />
<stop offset="1" stopColor="#DDCBA2" />
</linearGradient>
</defs>
<g transform="matrix(2.33,0,0,2.33,-432,-323)">
<g transform="translate(-25,26)">
<path
className="gotify-st1"
d="m258.9,119.7c-3,-0.9-6,-1.8-9,-2.7-4.6,-1.4-9.2,-2.8-14,-2.5-2.8,0.2-6.1,1.3-6.9,4-0.6,2-1.6,7.3-1.3,7.9 1.5,3.4 13.9,6.7 18.3,6.7"
/>
<path d="m392.6,177.9c-1.4,1.4-2.2,3.5-2.5,5.5-0.2,1.4-0.1,3 0.5,4.3 0.6,1.3 1.8,2.3 3.1,3 1.3,0.6 2.8,0.9 4.3,0.9 1.1,0 2.3,-0.1 3.1,-0.9 0.6,-0.7 0.8,-1.6 0.9,-2.5 0.2,-2.3-0.1,-4.7-0.9,-6.9-0.4,-1.1-0.9,-2.3-1.8,-3.1-1.7,-1.8-4.5,-2.2-6.4,-0.5-0.1,0-0.2,0.1-0.3,0.2z" />
<path
className="gotify-st2"
d="m358.5,164.2c-1,-1 0,-2.7 1,-3.7 5.8,-5.2 15.1,-4.6 21.8,-0.6 10.9,6.6 15.6,19.9 17.2,32.5 0.6,5.2 0.9,10.6-0.5,15.7-1.4,5.1-4.6,9.9-9.3,12.1-1.1,0.5-2.3,0.9-3.4,0.5-1.1,-0.4-1.9,-1.8-1.2,-2.8-9.4,-13.6-19,-26.8-20.9,-43.2-0.5,-4.1-1.8,-7.4-4.7,-10.5z"
/>
<path
className="gotify-st1"
d="m220.1,133c34.6,-18 79.3,-19.6 112.2,-8.7 23.7,7.9 41.3,26.7 49.5,50 7.1,20.6 7.1,43.6 3,65.7-7.5,40.2-26.2,77.9-49,112.6-12.6,19-24.6,36-44.2,48.5-38.7,24.6-88.9,22.1-129.3,11.5-19.5,-5.1-38.4,-17.3-44.3,-37.3-3.8,-12.8-2.1,-27.6 4.6,-40 13.5,-24.8 46.2,-38.4 50.8,-67.9 1.4,-8.7-0.3,-17.3-1.6,-25.7-3.8,-23.4-5.4,-45.8 6.7,-68.7 9.5,-17.7 24.3,-31 41.7,-40z"
/>
<path
className="gotify-st2"
d="m264.5,174.9c-0.5,0.5-0.9,1-1.3,1.6-9,11.6-12,27.9-9.3,42.1 1.7,9 5.9,17.9 13.2,23.4 19.3,14.6 51.5,13.5 68.4,-1.5 24.4,-21.7 13,-67.6-14,-78.8-17.6,-7.2-43.7,-1.6-57,13.2z"
/>
<path
className="gotify-st2"
d="m382.1,237.1c1.4,-0.1 2.9,-0.1 4.3,0.1 0.3,0 0.7,0.1 1,0.4 0.2,0.3 0.4,0.7 0.5,1.1 1,3.9 0.5,8.2 0.1,12.4-0.1,0.9-0.2,1.8-0.6,2.6-1,2.1-3.1,2.7-4.7,2.7-0.1,0-0.2,0-0.3,-0.1-0.3,-0.2-0.3,-0.7-0.2,-1.2 0.3,-5.9-0.1,-11.9-0.1,-18z"
/>
<path
className="gotify-st2"
d="m378.7,236.8c-1.4,0.4-2.5,2-2.8,4.4-0.5,4.4-0.7,8.9-0.5,13.4 0,0.9 0.1,1.9 0.5,2.4 0.2,0.3 0.5,0.4 0.8,0.4 1.6,0.3 4.1,-0.6 5.6,-1 0,0 0,-5.2-0.1,-8-0.1,-2.8-0.1,-6.1-0.2,-8.9 0,-0.6 0,-1.5 0,-2.2 0.1,-0.7-2.6,-0.7-3.3,-0.5z"
/>
<path
className="gotify-st0"
d="m358.3,231.8c-0.3,2.2 0.1,4.7 1.7,7.4 2.6,4.4 7,6.1 11.9,5.8 8.9,-0.6 25.3,-5.4 27.5,-15.7 0.6,-3-0.3,-6.1-2.2,-8.5-6.2,-7.8-17.8,-5.7-25.6,-2-5.9,2.7-12.4,7-13.3,13z"
/>
<path
className="gotify-st3"
d="m386.4,208.6c2.2,1.4 3.7,3.8 4,7 0.3,3.6-1.4,7.5-5,8.8-2.9,1.1-6.2,0.6-9.1,-0.4-2.9,-1-5.8,-2.8-6.8,-5.7-0.7,-2-0.3,-4.3 0.7,-6.1 1.1,-1.8 2.8,-3.2 4.7,-4.1 3.9,-1.8 8.4,-1.6 11.5,0.5z"
/>
<path
className="gotify-st0"
d="m414.7,262.6c2.4,0.6 4.8,2.1 5.6,4.4 0.8,2.3 0.1,4.9-1.6,6.7-1.7,1.8-4.2,2.5-6.6,2.5-0.8,0-1.7,-0.1-2.4,-0.5-2.5,-1.1-3.5,-4-4.2,-6.6-1.8,-6.8 3.6,-7.8 9.2,-6.5z"
/>
<path
className="gotify-st4"
d="m267.1,284.7c2.3,-4.5 141.3,-36.2 144.7,-31.6 3.4,4.5 15.8,88.2 9,90.4-6.8,2.3-119.8,37.3-126.6,35-6.8,-2.3-29.4,-89.3-27.1,-93.8z"
/>
<path
className="gotify-st5"
d="m294.2,378.5c0,0 54.3,-74.6 59.9,-76.9 5.7,-2.3 67.3,41.3 67.3,41.3"
/>
<path
className="gotify-st4"
d="m267,287.7c0,0 86,38.8 91.6,36.6 5.7,-2.3 53.1,-71.2 53.1,-71.2"
/>
<path
fill="url(#gotify-gradient)"
d="m261.9,283.5c-0.1,4.2 4.3,7.3 8.4,7.6 4.1,0.3 8.2,-1.3 12.2,-2.6 1.4,-0.4 2.9,-0.8 4.2,-0.2 1.8,0.9 2.7,4.1 1.8,5.9-0.9,1.8-3.4,3.5-5.3,4.4-6.5,3-12.9,3.6-19.9,2-5.3,-1.2-11.3,-4.3-13,-13.5"
/>
<path d="m318.4,198.4c-2,-0.3-4.1,0.1-5.9,1.3-3.2,2.1-4.7,6.2-4.7,9.9 0,1.9 0.4,3.8 1.4,5.3 1.2,1.7 3.1,2.9 5.2,3.4 3.4,0.8 8.2,0.7 10.5,-2.5 1,-1.5 1.4,-3.3 1.5,-5.1 0.5,-5.7-1.8,-11.4-8,-12.3z" />
<path
className="gotify-st8"
d="m320.4,203.3c0.9,0.3 1.7,0.8 2.1,1.7 0.4,0.8 0.4,1.7 0.3,2.5-0.1,1-0.6,2-1.5,2.7-0.7,0.5-1.7,0.7-2.6,0.5-0.9,-0.2-1.7,-0.8-2.2,-1.6-1.1,-1.6-0.9,-4.4 0.9,-5.5 0.9,-0.4 2,-0.6 3,-0.3z"
/>
</g>
</g>
</svg>
);
};
export const NtfyIcon = ({ className }: Props) => {
return (
<svg
viewBox="0 0 24 24"
className={cn("size-8", className)}
xmlns="http://www.w3.org/2000/svg"
>
<path
fill="currentColor"
d="M12.597 13.693v2.156h6.205v-2.156ZM5.183 6.549v2.363l3.591 1.901 0.023 0.01 -0.023 0.009 -3.591 1.901v2.35l0.386 -0.211 5.456 -2.969V9.729ZM3.659 2.037C1.915 2.037 0.42 3.41 0.42 5.154v0.002L0.438 18.73 0 21.963l5.956 -1.583h14.806c1.744 0 3.238 -1.374 3.238 -3.118V5.154c0 -1.744 -1.493 -3.116 -3.237 -3.117h-0.001zm0 2.2h17.104c0.613 0.001 1.037 0.447 1.037 0.917v12.108c0 0.47 -0.424 0.916 -1.038 0.916H5.633l-3.026 0.915 0.031 -0.179 -0.017 -13.76c0 -0.47 0.424 -0.917 1.038 -0.917z"
/>
</svg>
);
};

View File

@@ -7,7 +7,14 @@ import {
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
interface Props { interface Props {
status: "running" | "error" | "done" | "idle" | undefined | null; status:
| "running"
| "error"
| "done"
| "idle"
| "cancelled"
| undefined
| null;
className?: string; className?: string;
} }
@@ -34,6 +41,14 @@ export const StatusTooltip = ({ status, className }: Props) => {
className={cn("size-3.5 rounded-full bg-green-500", className)} className={cn("size-3.5 rounded-full bg-green-500", className)}
/> />
)} )}
{status === "cancelled" && (
<div
className={cn(
"size-3.5 rounded-full bg-muted-foreground",
className,
)}
/>
)}
{status === "running" && ( {status === "running" && (
<div <div
className={cn("size-3.5 rounded-full bg-yellow-500", className)} className={cn("size-3.5 rounded-full bg-yellow-500", className)}
@@ -46,6 +61,7 @@ export const StatusTooltip = ({ status, className }: Props) => {
{status === "error" && "Error"} {status === "error" && "Error"}
{status === "done" && "Done"} {status === "done" && "Done"}
{status === "running" && "Running"} {status === "running" && "Running"}
{status === "cancelled" && "Cancelled"}
</span> </span>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>

View File

@@ -0,0 +1 @@
ALTER TABLE "bitbucket" ADD COLUMN "apiToken" text;

View File

@@ -0,0 +1 @@
ALTER TABLE "bitbucket" ADD COLUMN "bitbucketEmail" text;

View File

@@ -0,0 +1 @@
ALTER TYPE "public"."deploymentStatus" ADD VALUE 'cancelled';

View File

@@ -0,0 +1,6 @@
ALTER TABLE "application" ADD COLUMN "stopGracePeriodSwarm" bigint;--> statement-breakpoint
ALTER TABLE "mariadb" ADD COLUMN "stopGracePeriodSwarm" bigint;--> statement-breakpoint
ALTER TABLE "mongo" ADD COLUMN "stopGracePeriodSwarm" bigint;--> statement-breakpoint
ALTER TABLE "mysql" ADD COLUMN "stopGracePeriodSwarm" bigint;--> statement-breakpoint
ALTER TABLE "postgres" ADD COLUMN "stopGracePeriodSwarm" bigint;--> statement-breakpoint
ALTER TABLE "redis" ADD COLUMN "stopGracePeriodSwarm" bigint;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -778,6 +778,34 @@
"when": 1757189541734, "when": 1757189541734,
"tag": "0110_red_psynapse", "tag": "0110_red_psynapse",
"breakpoints": true "breakpoints": true
},
{
"idx": 111,
"version": "7",
"when": 1758445844561,
"tag": "0111_mushy_wolfsbane",
"breakpoints": true
},
{
"idx": 112,
"version": "7",
"when": 1758483520214,
"tag": "0112_freezing_skrulls",
"breakpoints": true
},
{
"idx": 113,
"version": "7",
"when": 1758960816504,
"tag": "0113_complete_rafael_vega",
"breakpoints": true
},
{
"idx": 114,
"version": "7",
"when": 1759643172958,
"tag": "0114_dry_black_tom",
"breakpoints": true
} }
] ]
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "dokploy", "name": "dokploy",
"version": "v0.25.1", "version": "v0.25.4",
"private": true, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
@@ -112,7 +112,7 @@
"i18next": "^23.16.8", "i18next": "^23.16.8",
"input-otp": "^1.4.2", "input-otp": "^1.4.2",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"js-yaml": "4.1.0", "yaml": "2.8.1",
"lodash": "4.17.21", "lodash": "4.17.21",
"lucide-react": "^0.469.0", "lucide-react": "^0.469.0",
"micromatch": "4.0.8", "micromatch": "4.0.8",
@@ -160,7 +160,6 @@
"@types/adm-zip": "^0.5.7", "@types/adm-zip": "^0.5.7",
"@types/bcrypt": "5.0.2", "@types/bcrypt": "5.0.2",
"@types/js-cookie": "^3.0.6", "@types/js-cookie": "^3.0.6",
"@types/js-yaml": "4.0.9",
"@types/lodash": "4.17.4", "@types/lodash": "4.17.4",
"@types/micromatch": "4.0.9", "@types/micromatch": "4.0.9",
"@types/node": "^18.19.104", "@types/node": "^18.19.104",

View File

@@ -1,4 +1,9 @@
import { IS_CLOUD, shouldDeploy } from "@dokploy/server"; import {
type Bitbucket,
getBitbucketHeaders,
IS_CLOUD,
shouldDeploy,
} from "@dokploy/server";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import { db } from "@/server/db"; import { db } from "@/server/db";
@@ -146,10 +151,10 @@ export default async function handler(
const commitedPaths = await extractCommitedPaths( const commitedPaths = await extractCommitedPaths(
req.body, req.body,
application.bitbucketOwner, application.bitbucket,
application.bitbucket?.appPassword || "",
application.bitbucketRepository || "", application.bitbucketRepository || "",
); );
const shouldDeployPaths = shouldDeploy( const shouldDeployPaths = shouldDeploy(
application.watchPaths, application.watchPaths,
commitedPaths, commitedPaths,
@@ -354,9 +359,8 @@ export const getProviderByHeader = (headers: any) => {
export const extractCommitedPaths = async ( export const extractCommitedPaths = async (
body: any, body: any,
bitbucketUsername: string | null, bitbucket: Bitbucket | null,
bitbucketAppPassword: string | null, repository: string,
repository: string | null,
) => { ) => {
const changes = body.push?.changes || []; const changes = body.push?.changes || [];
@@ -365,18 +369,16 @@ export const extractCommitedPaths = async (
.filter(Boolean); .filter(Boolean);
const commitedPaths: string[] = []; const commitedPaths: string[] = [];
for (const commit of commitHashes) { for (const commit of commitHashes) {
const url = `https://api.bitbucket.org/2.0/repositories/${bitbucketUsername}/${repository}/diffstat/${commit}`; const url = `https://api.bitbucket.org/2.0/repositories/${bitbucket?.bitbucketUsername}/${repository}/diffstat/${commit}`;
try { try {
const response = await fetch(url, { const response = await fetch(url, {
headers: { headers: getBitbucketHeaders(bitbucket!),
Authorization: `Basic ${Buffer.from(`${bitbucketUsername}:${bitbucketAppPassword}`).toString("base64")}`,
},
}); });
const data = await response.json(); const data = await response.json();
for (const value of data.values) { for (const value of data.values) {
commitedPaths.push(value.new?.path); if (value?.new?.path) commitedPaths.push(value.new.path);
} }
} catch (error) { } catch (error) {
console.error( console.error(

View File

@@ -99,8 +99,7 @@ export default async function handler(
const commitedPaths = await extractCommitedPaths( const commitedPaths = await extractCommitedPaths(
req.body, req.body,
composeResult.bitbucketOwner, composeResult.bitbucket,
composeResult.bitbucket?.appPassword || "",
composeResult.bitbucketRepository || "", composeResult.bitbucketRepository || "",
); );

View File

@@ -226,6 +226,7 @@ const Service = (
<TabsTrigger value="general">General</TabsTrigger> <TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger> <TabsTrigger value="environment">Environment</TabsTrigger>
<TabsTrigger value="domains">Domains</TabsTrigger> <TabsTrigger value="domains">Domains</TabsTrigger>
<TabsTrigger value="deployments">Deployments</TabsTrigger>
<TabsTrigger value="preview-deployments"> <TabsTrigger value="preview-deployments">
Preview Deployments Preview Deployments
</TabsTrigger> </TabsTrigger>
@@ -233,7 +234,6 @@ const Service = (
<TabsTrigger value="volume-backups"> <TabsTrigger value="volume-backups">
Volume Backups Volume Backups
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="deployments">Deployments</TabsTrigger>
<TabsTrigger value="logs">Logs</TabsTrigger> <TabsTrigger value="logs">Logs</TabsTrigger>
{((data?.serverId && isCloud) || !data?.server) && ( {((data?.serverId && isCloud) || !data?.server) && (
<TabsTrigger value="monitoring">Monitoring</TabsTrigger> <TabsTrigger value="monitoring">Monitoring</TabsTrigger>

View File

@@ -524,7 +524,7 @@ export const applicationRouter = createTRPCRouter({
return true; return true;
}), }),
saveGitProdiver: protectedProcedure saveGitProvider: protectedProcedure
.input(apiSaveGitProvider) .input(apiSaveGitProvider)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId); const application = await findApplicationById(input.applicationId);

View File

@@ -7,7 +7,7 @@ import {
} from "@dokploy/server"; } from "@dokploy/server";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod";
import { getPublicIpWithFallback } from "@/server/wss/terminal"; import { getLocalServerIp } from "@/server/wss/terminal";
import { createTRPCRouter, protectedProcedure } from "../trpc"; import { createTRPCRouter, protectedProcedure } from "../trpc";
export const clusterRouter = createTRPCRouter({ export const clusterRouter = createTRPCRouter({
getNodes: protectedProcedure getNodes: protectedProcedure
@@ -61,7 +61,7 @@ export const clusterRouter = createTRPCRouter({
const result = await docker.swarmInspect(); const result = await docker.swarmInspect();
const docker_version = await docker.version(); const docker_version = await docker.version();
let ip = await getPublicIpWithFallback(); let ip = await getLocalServerIp();
if (input.serverId) { if (input.serverId) {
const server = await findServerById(input.serverId); const server = await findServerById(input.serverId);
ip = server?.ipAddress; ip = server?.ipAddress;
@@ -85,7 +85,7 @@ export const clusterRouter = createTRPCRouter({
const result = await docker.swarmInspect(); const result = await docker.swarmInspect();
const docker_version = await docker.version(); const docker_version = await docker.version();
let ip = await getPublicIpWithFallback(); let ip = await getLocalServerIp();
if (input.serverId) { if (input.serverId) {
const server = await findServerById(input.serverId); const server = await findServerById(input.serverId);
ip = server?.ipAddress; ip = server?.ipAddress;

View File

@@ -39,10 +39,10 @@ import {
import { processTemplate } from "@dokploy/server/templates/processors"; import { processTemplate } from "@dokploy/server/templates/processors";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { dump } from "js-yaml";
import _ from "lodash"; import _ from "lodash";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { parse } from "toml"; import { parse } from "toml";
import { stringify } from "yaml";
import { z } from "zod"; import { z } from "zod";
import { slugify } from "@/lib/slug"; import { slugify } from "@/lib/slug";
import { db } from "@/server/db"; import { db } from "@/server/db";
@@ -364,7 +364,7 @@ export const composeRouter = createTRPCRouter({
} }
const domains = await findDomainsByComposeId(input.composeId); const domains = await findDomainsByComposeId(input.composeId);
const composeFile = await addDomainToCompose(compose, domains); const composeFile = await addDomainToCompose(compose, domains);
return dump(composeFile, { return stringify(composeFile, {
lineWidth: 1000, lineWidth: 1000,
}); });
}), }),

View File

@@ -3,6 +3,7 @@ import {
getGithubBranches, getGithubBranches,
getGithubRepositories, getGithubRepositories,
haveGithubRequirements, haveGithubRequirements,
updateGithub,
updateGitProvider, updateGitProvider,
} from "@dokploy/server"; } from "@dokploy/server";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
@@ -134,5 +135,9 @@ export const githubRouter = createTRPCRouter({
name: input.name, name: input.name,
organizationId: ctx.session.activeOrganizationId, organizationId: ctx.session.activeOrganizationId,
}); });
await updateGithub(input.githubId, {
...input,
});
}), }),
}); });

View File

@@ -46,8 +46,8 @@ import {
import { generateOpenApiDocument } from "@dokploy/trpc-openapi"; import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { eq, sql } from "drizzle-orm"; import { eq, sql } from "drizzle-orm";
import { dump, load } from "js-yaml";
import { scheduledJobs, scheduleJob } from "node-schedule"; import { scheduledJobs, scheduleJob } from "node-schedule";
import { parse, stringify } from "yaml";
import { z } from "zod"; import { z } from "zod";
import { db } from "@/server/db"; import { db } from "@/server/db";
import { import {
@@ -657,7 +657,7 @@ export const settingsRouter = createTRPCRouter({
const config = readMainConfig(); const config = readMainConfig();
if (!config) return false; if (!config) return false;
const parsedConfig = load(config) as { const parsedConfig = parse(config) as {
accessLog?: { accessLog?: {
filePath: string; filePath: string;
}; };
@@ -678,7 +678,7 @@ export const settingsRouter = createTRPCRouter({
const mainConfig = readMainConfig(); const mainConfig = readMainConfig();
if (!mainConfig) return false; if (!mainConfig) return false;
const currentConfig = load(mainConfig) as { const currentConfig = parse(mainConfig) as {
accessLog?: { accessLog?: {
filePath: string; filePath: string;
}; };
@@ -701,7 +701,7 @@ export const settingsRouter = createTRPCRouter({
currentConfig.accessLog = undefined; currentConfig.accessLog = undefined;
} }
writeMainConfig(dump(currentConfig)); writeMainConfig(stringify(currentConfig));
return true; return true;
}), }),

View File

@@ -192,7 +192,16 @@ export const userRouter = createTRPCRouter({
}) })
.where(eq(account.userId, ctx.user.id)); .where(eq(account.userId, ctx.user.id));
} }
return await updateUser(ctx.user.id, input);
try {
return await updateUser(ctx.user.id, input);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message:
error instanceof Error ? error.message : "Failed to update user",
});
}
}), }),
getUserByToken: publicProcedure getUserByToken: publicProcedure
.input(apiFindOneToken) .input(apiFindOneToken)

View File

@@ -8,6 +8,7 @@ import {
initializeNetwork, initializeNetwork,
initSchedules, initSchedules,
initVolumeBackupsCronJobs, initVolumeBackupsCronJobs,
initCancelDeployments,
sendDokployRestartNotifications, sendDokployRestartNotifications,
setupDirectories, setupDirectories,
} from "@dokploy/server"; } from "@dokploy/server";
@@ -52,6 +53,7 @@ void app.prepare().then(async () => {
await migration(); await migration();
await initCronJobs(); await initCronJobs();
await initSchedules(); await initSchedules();
await initCancelDeployments();
await initVolumeBackupsCronJobs(); await initVolumeBackupsCronJobs();
await sendDokployRestartNotifications(); await sendDokployRestartNotifications();
} }

View File

@@ -1,5 +1,10 @@
import type http from "node:http"; import type http from "node:http";
import { findServerById, IS_CLOUD, validateRequest } from "@dokploy/server"; import {
execAsync,
findServerById,
IS_CLOUD,
validateRequest,
} from "@dokploy/server";
import { publicIpv4, publicIpv6 } from "public-ip"; import { publicIpv4, publicIpv6 } from "public-ip";
import { Client, type ConnectConfig } from "ssh2"; import { Client, type ConnectConfig } from "ssh2";
import { WebSocketServer } from "ws"; import { WebSocketServer } from "ws";
@@ -44,6 +49,21 @@ export const getPublicIpWithFallback = async () => {
return ip; return ip;
}; };
export const getLocalServerIp = async () => {
try {
const command = `ip addr show | grep -E "inet (192\.168\.|10\.|172\.1[6-9]\.|172\.2[0-9]\.|172\.3[0-1]\.)" | head -n1 | awk '{print $2}' | cut -d/ -f1`;
const { stdout } = await execAsync(command);
const ip = stdout.trim();
return (
ip ||
"We were unable to obtain the local server IP, please use your private IP address"
);
} catch (error) {
console.error("Error to obtain local server IP", error);
return "We were unable to obtain the local server IP, please use your private IP address";
}
};
export const setupTerminalWebSocketServer = ( export const setupTerminalWebSocketServer = (
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>, server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
) => { ) => {

View File

@@ -1,3 +1,4 @@
import { exit } from "node:process";
import { execAsync } from "@dokploy/server"; import { execAsync } from "@dokploy/server";
import { setupDirectories } from "@dokploy/server/setup/config-paths"; import { setupDirectories } from "@dokploy/server/setup/config-paths";
import { initializePostgres } from "@dokploy/server/setup/postgres-setup"; import { initializePostgres } from "@dokploy/server/setup/postgres-setup";
@@ -25,6 +26,8 @@ import {
await initializeStandaloneTraefik(); await initializeStandaloneTraefik();
await initializeRedis(); await initializeRedis();
await initializePostgres(); await initializePostgres();
console.log("Dokploy setup completed");
exit(0);
} catch (e) { } catch (e) {
console.error("Error in dokploy setup", e); console.error("Error in dokploy setup", e);
} }

View File

@@ -11,7 +11,7 @@ import {
} from "./queue.js"; } from "./queue.js";
import { jobQueueSchema } from "./schema.js"; import { jobQueueSchema } from "./schema.js";
import { initializeJobs } from "./utils.js"; import { initializeJobs } from "./utils.js";
import { firstWorker, secondWorker } from "./workers.js"; import { firstWorker, secondWorker, thirdWorker } from "./workers.js";
const app = new Hono(); const app = new Hono();
@@ -91,6 +91,7 @@ export const gracefulShutdown = async (signal: string) => {
logger.warn(`Received ${signal}, closing server...`); logger.warn(`Received ${signal}, closing server...`);
await firstWorker.close(); await firstWorker.close();
await secondWorker.close(); await secondWorker.close();
await thirdWorker.close();
process.exit(0); process.exit(0);
}; };

View File

@@ -7,22 +7,34 @@ import { runJobs } from "./utils.js";
export const firstWorker = new Worker( export const firstWorker = new Worker(
"backupQueue", "backupQueue",
async (job: Job<QueueJob>) => { async (job: Job<QueueJob>) => {
logger.info({ data: job.data }, "Running job"); logger.info({ data: job.data }, "Running job first worker");
await runJobs(job.data); await runJobs(job.data);
}, },
{ {
concurrency: 50, concurrency: 100,
connection, connection,
}, },
); );
export const secondWorker = new Worker( export const secondWorker = new Worker(
"backupQueue", "backupQueue",
async (job: Job<QueueJob>) => { async (job: Job<QueueJob>) => {
logger.info({ data: job.data }, "Running job"); logger.info({ data: job.data }, "Running job second worker");
await runJobs(job.data); await runJobs(job.data);
}, },
{ {
concurrency: 50, concurrency: 100,
connection,
},
);
export const thirdWorker = new Worker(
"backupQueue",
async (job: Job<QueueJob>) => {
logger.info({ data: job.data }, "Running job third worker");
await runJobs(job.data);
},
{
concurrency: 100,
connection, connection,
}, },
); );

View File

@@ -57,7 +57,7 @@
"drizzle-orm": "^0.39.3", "drizzle-orm": "^0.39.3",
"drizzle-zod": "0.5.1", "drizzle-zod": "0.5.1",
"hi-base32": "^0.5.1", "hi-base32": "^0.5.1",
"js-yaml": "4.1.0", "yaml": "2.8.1",
"lodash": "4.17.21", "lodash": "4.17.21",
"micromatch": "4.0.8", "micromatch": "4.0.8",
"nanoid": "3.3.11", "nanoid": "3.3.11",
@@ -85,7 +85,6 @@
"@types/adm-zip": "^0.5.7", "@types/adm-zip": "^0.5.7",
"@types/bcrypt": "5.0.2", "@types/bcrypt": "5.0.2",
"@types/dockerode": "3.3.23", "@types/dockerode": "3.3.23",
"@types/js-yaml": "4.0.9",
"@types/lodash": "4.17.4", "@types/lodash": "4.17.4",
"@types/micromatch": "4.0.9", "@types/micromatch": "4.0.9",
"@types/node": "^18.19.104", "@types/node": "^18.19.104",

View File

@@ -1,5 +1,6 @@
import { relations } from "drizzle-orm"; import { relations } from "drizzle-orm";
import { import {
bigint,
boolean, boolean,
integer, integer,
json, json,
@@ -20,7 +21,6 @@ import { gitlab } from "./gitlab";
import { mounts } from "./mount"; import { mounts } from "./mount";
import { ports } from "./port"; import { ports } from "./port";
import { previewDeployments } from "./preview-deployments"; import { previewDeployments } from "./preview-deployments";
import { projects } from "./project";
import { redirects } from "./redirects"; import { redirects } from "./redirects";
import { registry } from "./registry"; import { registry } from "./registry";
import { security } from "./security"; import { security } from "./security";
@@ -164,6 +164,7 @@ export const applications = pgTable("application", {
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(), modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(), labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(), networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
// //
replicas: integer("replicas").default(1).notNull(), replicas: integer("replicas").default(1).notNull(),
applicationStatus: applicationStatus("applicationStatus") applicationStatus: applicationStatus("applicationStatus")
@@ -312,6 +313,7 @@ const createSchema = createInsertSchema(applications, {
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
previewLabels: z.array(z.string()).optional(), previewLabels: z.array(z.string()).optional(),
cleanCache: z.boolean().optional(), cleanCache: z.boolean().optional(),
stopGracePeriodSwarm: z.bigint().nullable(),
}); });
export const apiCreateApplication = createSchema.pick({ export const apiCreateApplication = createSchema.pick({

View File

@@ -11,7 +11,9 @@ export const bitbucket = pgTable("bitbucket", {
.primaryKey() .primaryKey()
.$defaultFn(() => nanoid()), .$defaultFn(() => nanoid()),
bitbucketUsername: text("bitbucketUsername"), bitbucketUsername: text("bitbucketUsername"),
bitbucketEmail: text("bitbucketEmail"),
appPassword: text("appPassword"), appPassword: text("appPassword"),
apiToken: text("apiToken"),
bitbucketWorkspaceName: text("bitbucketWorkspaceName"), bitbucketWorkspaceName: text("bitbucketWorkspaceName"),
gitProviderId: text("gitProviderId") gitProviderId: text("gitProviderId")
.notNull() .notNull()
@@ -29,7 +31,9 @@ const createSchema = createInsertSchema(bitbucket);
export const apiCreateBitbucket = createSchema.extend({ export const apiCreateBitbucket = createSchema.extend({
bitbucketUsername: z.string().optional(), bitbucketUsername: z.string().optional(),
bitbucketEmail: z.string().email().optional(),
appPassword: z.string().optional(), appPassword: z.string().optional(),
apiToken: z.string().optional(),
bitbucketWorkspaceName: z.string().optional(), bitbucketWorkspaceName: z.string().optional(),
gitProviderId: z.string().optional(), gitProviderId: z.string().optional(),
authId: z.string().min(1), authId: z.string().min(1),
@@ -46,9 +50,19 @@ export const apiBitbucketTestConnection = createSchema
.extend({ .extend({
bitbucketId: z.string().min(1), bitbucketId: z.string().min(1),
bitbucketUsername: z.string().optional(), bitbucketUsername: z.string().optional(),
bitbucketEmail: z.string().email().optional(),
workspaceName: z.string().optional(), workspaceName: z.string().optional(),
apiToken: z.string().optional(),
appPassword: z.string().optional(),
}) })
.pick({ bitbucketId: true, bitbucketUsername: true, workspaceName: true }); .pick({
bitbucketId: true,
bitbucketUsername: true,
bitbucketEmail: true,
workspaceName: true,
apiToken: true,
appPassword: true,
});
export const apiFindBitbucketBranches = z.object({ export const apiFindBitbucketBranches = z.object({
owner: z.string(), owner: z.string(),
@@ -60,6 +74,9 @@ export const apiUpdateBitbucket = createSchema.extend({
bitbucketId: z.string().min(1), bitbucketId: z.string().min(1),
name: z.string().min(1), name: z.string().min(1),
bitbucketUsername: z.string().optional(), bitbucketUsername: z.string().optional(),
bitbucketEmail: z.string().email().optional(),
appPassword: z.string().optional(),
apiToken: z.string().optional(),
bitbucketWorkspaceName: z.string().optional(), bitbucketWorkspaceName: z.string().optional(),
organizationId: z.string().optional(), organizationId: z.string().optional(),
}); });

View File

@@ -21,6 +21,7 @@ export const deploymentStatus = pgEnum("deploymentStatus", [
"running", "running",
"done", "done",
"error", "error",
"cancelled",
]); ]);
export const deployments = pgTable("deployment", { export const deployments = pgTable("deployment", {

View File

@@ -58,4 +58,5 @@ export const apiUpdateGithub = createSchema.extend({
githubId: z.string().min(1), githubId: z.string().min(1),
name: z.string().min(1), name: z.string().min(1),
gitProviderId: z.string().min(1), gitProviderId: z.string().min(1),
githubAppName: z.string().min(1),
}); });

View File

@@ -1,5 +1,5 @@
import { relations } from "drizzle-orm"; import { relations } from "drizzle-orm";
import { integer, json, pgTable, text } from "drizzle-orm/pg-core"; import { bigint, integer, json, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod"; import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
@@ -62,6 +62,7 @@ export const mariadb = pgTable("mariadb", {
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(), modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(), labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(), networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
replicas: integer("replicas").default(1).notNull(), replicas: integer("replicas").default(1).notNull(),
createdAt: text("createdAt") createdAt: text("createdAt")
.notNull() .notNull()
@@ -128,6 +129,7 @@ const createSchema = createInsertSchema(mariadb, {
modeSwarm: ServiceModeSwarmSchema.nullable(), modeSwarm: ServiceModeSwarmSchema.nullable(),
labelsSwarm: LabelsSwarmSchema.nullable(), labelsSwarm: LabelsSwarmSchema.nullable(),
networkSwarm: NetworkSwarmSchema.nullable(), networkSwarm: NetworkSwarmSchema.nullable(),
stopGracePeriodSwarm: z.bigint().nullable(),
}); });
export const apiCreateMariaDB = createSchema export const apiCreateMariaDB = createSchema

View File

@@ -1,5 +1,12 @@
import { relations } from "drizzle-orm"; import { relations } from "drizzle-orm";
import { boolean, integer, json, pgTable, text } from "drizzle-orm/pg-core"; import {
bigint,
boolean,
integer,
json,
pgTable,
text,
} from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod"; import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
@@ -58,6 +65,7 @@ export const mongo = pgTable("mongo", {
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(), modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(), labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(), networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
replicas: integer("replicas").default(1).notNull(), replicas: integer("replicas").default(1).notNull(),
createdAt: text("createdAt") createdAt: text("createdAt")
.notNull() .notNull()
@@ -118,6 +126,7 @@ const createSchema = createInsertSchema(mongo, {
modeSwarm: ServiceModeSwarmSchema.nullable(), modeSwarm: ServiceModeSwarmSchema.nullable(),
labelsSwarm: LabelsSwarmSchema.nullable(), labelsSwarm: LabelsSwarmSchema.nullable(),
networkSwarm: NetworkSwarmSchema.nullable(), networkSwarm: NetworkSwarmSchema.nullable(),
stopGracePeriodSwarm: z.bigint().nullable(),
}); });
export const apiCreateMongo = createSchema export const apiCreateMongo = createSchema

View File

@@ -1,5 +1,5 @@
import { relations } from "drizzle-orm"; import { relations } from "drizzle-orm";
import { integer, json, pgTable, text } from "drizzle-orm/pg-core"; import { bigint, integer, json, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod"; import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
@@ -60,6 +60,7 @@ export const mysql = pgTable("mysql", {
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(), modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(), labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(), networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
replicas: integer("replicas").default(1).notNull(), replicas: integer("replicas").default(1).notNull(),
createdAt: text("createdAt") createdAt: text("createdAt")
.notNull() .notNull()
@@ -125,6 +126,7 @@ const createSchema = createInsertSchema(mysql, {
modeSwarm: ServiceModeSwarmSchema.nullable(), modeSwarm: ServiceModeSwarmSchema.nullable(),
labelsSwarm: LabelsSwarmSchema.nullable(), labelsSwarm: LabelsSwarmSchema.nullable(),
networkSwarm: NetworkSwarmSchema.nullable(), networkSwarm: NetworkSwarmSchema.nullable(),
stopGracePeriodSwarm: z.bigint().nullable(),
}); });
export const apiCreateMySql = createSchema export const apiCreateMySql = createSchema

View File

@@ -1,5 +1,5 @@
import { relations } from "drizzle-orm"; import { relations } from "drizzle-orm";
import { integer, json, pgTable, text } from "drizzle-orm/pg-core"; import { bigint, integer, json, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod"; import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
@@ -60,6 +60,7 @@ export const postgres = pgTable("postgres", {
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(), modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(), labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(), networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
replicas: integer("replicas").default(1).notNull(), replicas: integer("replicas").default(1).notNull(),
createdAt: text("createdAt") createdAt: text("createdAt")
.notNull() .notNull()
@@ -118,6 +119,7 @@ const createSchema = createInsertSchema(postgres, {
modeSwarm: ServiceModeSwarmSchema.nullable(), modeSwarm: ServiceModeSwarmSchema.nullable(),
labelsSwarm: LabelsSwarmSchema.nullable(), labelsSwarm: LabelsSwarmSchema.nullable(),
networkSwarm: NetworkSwarmSchema.nullable(), networkSwarm: NetworkSwarmSchema.nullable(),
stopGracePeriodSwarm: z.bigint().nullable(),
}); });
export const apiCreatePostgres = createSchema export const apiCreatePostgres = createSchema

View File

@@ -1,5 +1,5 @@
import { relations } from "drizzle-orm"; import { relations } from "drizzle-orm";
import { integer, json, pgTable, text } from "drizzle-orm/pg-core"; import { bigint, integer, json, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod"; import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
@@ -60,6 +60,7 @@ export const redis = pgTable("redis", {
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(), modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(), labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(), networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
replicas: integer("replicas").default(1).notNull(), replicas: integer("replicas").default(1).notNull(),
environmentId: text("environmentId") environmentId: text("environmentId")
@@ -108,6 +109,7 @@ const createSchema = createInsertSchema(redis, {
modeSwarm: ServiceModeSwarmSchema.nullable(), modeSwarm: ServiceModeSwarmSchema.nullable(),
labelsSwarm: LabelsSwarmSchema.nullable(), labelsSwarm: LabelsSwarmSchema.nullable(),
networkSwarm: NetworkSwarmSchema.nullable(), networkSwarm: NetworkSwarmSchema.nullable(),
stopGracePeriodSwarm: z.bigint().nullable(),
}); });
export const apiCreateRedis = createSchema export const apiCreateRedis = createSchema

View File

@@ -323,6 +323,11 @@ export const apiUpdateWebServerMonitoring = z.object({
}); });
export const apiUpdateUser = createSchema.partial().extend({ export const apiUpdateUser = createSchema.partial().extend({
email: z
.string()
.email("Please enter a valid email address")
.min(1, "Email is required")
.optional(),
password: z.string().optional(), password: z.string().optional(),
currentPassword: z.string().optional(), currentPassword: z.string().optional(),
name: z.string().optional(), name: z.string().optional(),

View File

@@ -68,6 +68,7 @@ export * from "./utils/backups/postgres";
export * from "./utils/backups/utils"; export * from "./utils/backups/utils";
export * from "./utils/backups/web-server"; export * from "./utils/backups/web-server";
export * from "./utils/builders/compose"; export * from "./utils/builders/compose";
export * from "./utils/startup/cancell-deployments";
export * from "./utils/builders/docker-file"; export * from "./utils/builders/docker-file";
export * from "./utils/builders/drop"; export * from "./utils/builders/drop";
export * from "./utils/builders/heroku"; export * from "./utils/builders/heroku";

View File

@@ -92,31 +92,48 @@ export const suggestVariants = async ({
const { object } = await generateObject({ const { object } = await generateObject({
model, model,
output: "array", output: "object",
schema: z.object({ schema: z.object({
id: z.string(), suggestions: z.array(
name: z.string(), z.object({
shortDescription: z.string(), id: z.string(),
description: z.string(), name: z.string(),
shortDescription: z.string(),
description: z.string(),
}),
),
}), }),
prompt: ` prompt: `
Act as advanced DevOps engineer and generate a list of open source projects what can cover users needs(up to 3 items), the suggestion Act as advanced DevOps engineer and generate a list of open source projects what can cover users needs(up to 3 items).
should include id, name, shortDescription, and description. Use slug of title for id.
Return your response as a JSON object with the following structure:
{
"suggestions": [
{
"id": "project-slug",
"name": "Project Name",
"shortDescription": "Brief one-line description",
"description": "Detailed description"
}
]
}
Important rules for the response: Important rules for the response:
1. The description field should ONLY contain a plain text description of the project, its features, and use cases 1. Use slug format for the id field (lowercase, hyphenated)
2. Do NOT include any code snippets, configuration examples, or installation instructions in the description 2. The description field should ONLY contain a plain text description of the project, its features, and use cases
3. The shortDescription should be a single-line summary focusing on the main technologies 3. Do NOT include any code snippets, configuration examples, or installation instructions in the description
4. The shortDescription should be a single-line summary focusing on the main technologies
5. All projects should be installable in docker and have docker compose support
User wants to create a new project with the following details, it should be installable in docker and can be docker compose generated for it: User wants to create a new project with the following details:
${input} ${input}
`, `,
}); });
if (object?.length) { if (object?.suggestions?.length) {
const result = []; const result = [];
for (const suggestion of object) { for (const suggestion of object.suggestions) {
try { try {
const { object: docker } = await generateObject({ const { object: docker } = await generateObject({
model, model,
@@ -136,16 +153,29 @@ export const suggestVariants = async ({
serviceName: z.string(), serviceName: z.string(),
}), }),
), ),
configFiles: z.array( configFiles: z
z.object({ .array(
content: z.string(), z.object({
filePath: z.string(), content: z.string(),
}), filePath: z.string(),
), }),
)
.optional(),
}), }),
prompt: ` prompt: `
Act as advanced DevOps engineer and generate docker compose with environment variables and domain configurations needed to install the following project. Act as advanced DevOps engineer and generate docker compose with environment variables and domain configurations needed to install the following project.
Return the docker compose as a YAML string and environment variables configuration. Follow these rules:
Return your response as a JSON object with this structure:
{
"dockerCompose": "yaml string here",
"envVariables": [{"name": "VAR_NAME", "value": "example_value"}],
"domains": [{"host": "domain.com", "port": 3000, "serviceName": "service"}],
"configFiles": [{"content": "file content", "filePath": "path/to/file"}]
}
Note: configFiles is optional - only include it if configuration files are absolutely required.
Follow these rules:
Docker Compose Rules: Docker Compose Rules:
1. Use placeholder like \${VARIABLE_NAME-default} for generated variables in the docker-compose.yml 1. Use placeholder like \${VARIABLE_NAME-default} for generated variables in the docker-compose.yml
@@ -198,6 +228,7 @@ export const suggestVariants = async ({
console.error("Error in docker compose generation:", error); console.error("Error in docker compose generation:", error);
} }
} }
return result; return result;
} }

View File

@@ -68,10 +68,26 @@ export const updateBitbucket = async (
input: typeof apiUpdateBitbucket._type, input: typeof apiUpdateBitbucket._type,
) => { ) => {
return await db.transaction(async (tx) => { return await db.transaction(async (tx) => {
// First get the current bitbucket provider to get gitProviderId
const currentProvider = await tx.query.bitbucket.findFirst({
where: eq(bitbucket.bitbucketId, bitbucketId),
});
if (!currentProvider) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Bitbucket provider not found",
});
}
const result = await tx const result = await tx
.update(bitbucket) .update(bitbucket)
.set({ .set({
...input, bitbucketUsername: input.bitbucketUsername,
bitbucketEmail: input.bitbucketEmail,
appPassword: input.appPassword,
apiToken: input.apiToken,
bitbucketWorkspaceName: input.bitbucketWorkspaceName,
}) })
.where(eq(bitbucket.bitbucketId, bitbucketId)) .where(eq(bitbucket.bitbucketId, bitbucketId))
.returning(); .returning();
@@ -83,7 +99,7 @@ export const updateBitbucket = async (
name: input.name, name: input.name,
organizationId: input.organizationId, organizationId: input.organizationId,
}) })
.where(eq(gitProvider.gitProviderId, input.gitProviderId)) .where(eq(gitProvider.gitProviderId, currentProvider.gitProviderId))
.returning(); .returning();
} }

View File

@@ -603,6 +603,21 @@ const BUNNY_CDN_IPS = new Set([
"89.187.184.176", "89.187.184.176",
]); ]);
// Arvancloud IP ranges
// https://www.arvancloud.ir/fa/ips.txt
const ARVANCLOUD_IP_RANGES = [
"185.143.232.0/22",
"188.229.116.16/29",
"94.101.182.0/27",
"2.144.3.128/28",
"89.45.48.64/28",
"37.32.16.0/27",
"37.32.17.0/27",
"37.32.18.0/27",
"37.32.19.0/27",
"185.215.232.0/22",
];
const CDN_PROVIDERS: CDNProvider[] = [ const CDN_PROVIDERS: CDNProvider[] = [
{ {
name: "cloudflare", name: "cloudflare",
@@ -627,6 +642,14 @@ const CDN_PROVIDERS: CDNProvider[] = [
warningMessage: warningMessage:
"Domain is behind Fastly - actual IP is masked by CDN proxy", "Domain is behind Fastly - actual IP is masked by CDN proxy",
}, },
{
name: "arvancloud",
displayName: "Arvancloud",
checkIp: (ip: string) =>
ARVANCLOUD_IP_RANGES.some((range) => isIPInCIDR(ip, range)),
warningMessage:
"Domain is behind Arvancloud - actual IP is masked by CDN proxy",
},
]; ];
export const detectCDNProvider = (ip: string): CDNProvider | null => { export const detectCDNProvider = (ip: string): CDNProvider | null => {

View File

@@ -9,7 +9,7 @@ import {
import { removeDirectoryIfExistsContent } from "@dokploy/server/utils/filesystem/directory"; import { removeDirectoryIfExistsContent } from "@dokploy/server/utils/filesystem/directory";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { dump } from "js-yaml"; import { stringify } from "yaml";
import type { z } from "zod"; import type { z } from "zod";
import { encodeBase64 } from "../utils/docker/utils"; import { encodeBase64 } from "../utils/docker/utils";
import { execAsyncRemote } from "../utils/process/execAsync"; import { execAsyncRemote } from "../utils/process/execAsync";
@@ -101,7 +101,7 @@ const createCertificateFiles = async (certificate: Certificate) => {
], ],
}, },
}; };
const yamlConfig = dump(traefikConfig); const yamlConfig = stringify(traefikConfig);
const configFile = path.join(certDir, "certificate.yml"); const configFile = path.join(certDir, "certificate.yml");
if (certificate.serverId) { if (certificate.serverId) {

View File

@@ -227,7 +227,7 @@ export const deployCompose = async ({
const buildLink = `${await getDokployUrl()}/dashboard/project/${ const buildLink = `${await getDokployUrl()}/dashboard/project/${
compose.environment.projectId compose.environment.projectId
}/services/compose/${compose.composeId}?tab=deployments`; }/environment/${compose.environmentId}/services/compose/${compose.composeId}?tab=deployments`;
const deployment = await createDeploymentCompose({ const deployment = await createDeploymentCompose({
composeId: composeId, composeId: composeId,
title: titleLog, title: titleLog,
@@ -335,7 +335,7 @@ export const deployRemoteCompose = async ({
const buildLink = `${await getDokployUrl()}/dashboard/project/${ const buildLink = `${await getDokployUrl()}/dashboard/project/${
compose.environment.projectId compose.environment.projectId
}/services/compose/${compose.composeId}?tab=deployments`; }/environment/${compose.environmentId}/services/compose/${compose.composeId}?tab=deployments`;
const deployment = await createDeploymentCompose({ const deployment = await createDeploymentCompose({
composeId: composeId, composeId: composeId,
title: titleLog, title: titleLog,

View File

@@ -10,6 +10,22 @@ import { IS_CLOUD } from "../constants";
export type Registry = typeof registry.$inferSelect; export type Registry = typeof registry.$inferSelect;
function shEscape(s: string | undefined): string {
if (!s) return "''";
return `'${s.replace(/'/g, `'\\''`)}'`;
}
function safeDockerLoginCommand(
registry: string | undefined,
user: string | undefined,
pass: string | undefined,
) {
const escapedRegistry = shEscape(registry);
const escapedUser = shEscape(user);
const escapedPassword = shEscape(pass);
return `printf %s ${escapedPassword} | docker login ${escapedRegistry} -u ${escapedUser} --password-stdin`;
}
export const createRegistry = async ( export const createRegistry = async (
input: typeof apiCreateRegistry._type, input: typeof apiCreateRegistry._type,
organizationId: string, organizationId: string,
@@ -37,7 +53,11 @@ export const createRegistry = async (
message: "Select a server to add the registry", message: "Select a server to add the registry",
}); });
} }
const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`; const loginCommand = safeDockerLoginCommand(
input.registryUrl,
input.username,
input.password,
);
if (input.serverId && input.serverId !== "none") { if (input.serverId && input.serverId !== "none") {
await execAsyncRemote(input.serverId, loginCommand); await execAsyncRemote(input.serverId, loginCommand);
} else if (newRegistry.registryType === "cloud") { } else if (newRegistry.registryType === "cloud") {
@@ -91,7 +111,11 @@ export const updateRegistry = async (
.returning() .returning()
.then((res) => res[0]); .then((res) => res[0]);
const loginCommand = `echo ${response?.password} | docker login ${response?.registryUrl} --username ${response?.username} --password-stdin`; const loginCommand = safeDockerLoginCommand(
response?.registryUrl,
response?.username,
response?.password,
);
if ( if (
IS_CLOUD && IS_CLOUD &&

View File

@@ -336,6 +336,19 @@ export const findMemberById = async (
}; };
export const updateUser = async (userId: string, userData: Partial<User>) => { export const updateUser = async (userId: string, userData: Partial<User>) => {
// Validate email if it's being updated
if (userData.email !== undefined) {
if (!userData.email || userData.email.trim() === "") {
throw new Error("Email is required and cannot be empty");
}
// Basic email format validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(userData.email)) {
throw new Error("Please enter a valid email address");
}
}
const user = await db const user = await db
.update(users_temp) .update(users_temp)
.set({ .set({

View File

@@ -1,7 +1,14 @@
import { chmodSync, existsSync, mkdirSync, writeFileSync } from "node:fs"; import {
chmodSync,
existsSync,
mkdirSync,
rmSync,
statSync,
writeFileSync,
} from "node:fs";
import path from "node:path"; import path from "node:path";
import type { ContainerCreateOptions, CreateServiceOptions } from "dockerode"; import type { ContainerCreateOptions, CreateServiceOptions } from "dockerode";
import { dump } from "js-yaml"; import { stringify } from "yaml";
import { paths } from "../constants"; import { paths } from "../constants";
import { getRemoteDocker } from "../utils/servers/remote-docker"; import { getRemoteDocker } from "../utils/servers/remote-docker";
import type { FileConfig } from "../utils/traefik/file-types"; import type { FileConfig } from "../utils/traefik/file-types";
@@ -234,7 +241,7 @@ export const createDefaultServerTraefikConfig = () => {
}, },
}; };
const yamlStr = dump(config); const yamlStr = stringify(config);
mkdirSync(DYNAMIC_TRAEFIK_PATH, { recursive: true }); mkdirSync(DYNAMIC_TRAEFIK_PATH, { recursive: true });
writeFileSync( writeFileSync(
path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`), path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`),
@@ -308,7 +315,7 @@ export const getDefaultTraefikConfig = () => {
}), }),
}; };
const yamlStr = dump(configObject); const yamlStr = stringify(configObject);
return yamlStr; return yamlStr;
}; };
@@ -362,7 +369,7 @@ export const getDefaultServerTraefikConfig = () => {
}, },
}; };
const yamlStr = dump(configObject); const yamlStr = stringify(configObject);
return yamlStr; return yamlStr;
}; };
@@ -375,13 +382,26 @@ export const createDefaultTraefikConfig = () => {
if (existsSync(acmeJsonPath)) { if (existsSync(acmeJsonPath)) {
chmodSync(acmeJsonPath, "600"); chmodSync(acmeJsonPath, "600");
} }
if (existsSync(mainConfig)) {
console.log("Main config already exists"); // Create the traefik directory first
return;
}
const yamlStr = getDefaultTraefikConfig();
mkdirSync(MAIN_TRAEFIK_PATH, { recursive: true }); mkdirSync(MAIN_TRAEFIK_PATH, { recursive: true });
// Check if traefik.yml exists and handle the case where it might be a directory
if (existsSync(mainConfig)) {
const stats = statSync(mainConfig);
if (stats.isDirectory()) {
// If traefik.yml is a directory, remove it
console.log("Found traefik.yml as directory, removing it...");
rmSync(mainConfig, { recursive: true, force: true });
} else if (stats.isFile()) {
console.log("Main config already exists");
return;
}
}
const yamlStr = getDefaultTraefikConfig();
writeFileSync(mainConfig, yamlStr, "utf8"); writeFileSync(mainConfig, yamlStr, "utf8");
console.log("Traefik config created successfully");
}; };
export const getDefaultMiddlewares = () => { export const getDefaultMiddlewares = () => {
@@ -397,7 +417,7 @@ export const getDefaultMiddlewares = () => {
}, },
}, },
}; };
const yamlStr = dump(defaultMiddlewares); const yamlStr = stringify(defaultMiddlewares);
return yamlStr; return yamlStr;
}; };
export const createDefaultMiddlewares = () => { export const createDefaultMiddlewares = () => {

View File

@@ -89,7 +89,7 @@ export const getMariadbBackupCommand = (
databaseUser: string, databaseUser: string,
databasePassword: string, databasePassword: string,
) => { ) => {
return `docker exec -i $CONTAINER_ID bash -c "set -o pipefail; mariadb-dump --user='${databaseUser}' --password='${databasePassword}' --databases ${database} | gzip"`; return `docker exec -i $CONTAINER_ID bash -c "set -o pipefail; mariadb-dump --user='${databaseUser}' --password='${databasePassword}' --single-transaction --quick --databases ${database} | gzip"`;
}; };
export const getMysqlBackupCommand = ( export const getMysqlBackupCommand = (

View File

@@ -142,6 +142,7 @@ export const mechanizeDockerContainer = async (
RollbackConfig, RollbackConfig,
UpdateConfig, UpdateConfig,
Networks, Networks,
StopGracePeriod,
} = generateConfigContainer(application); } = generateConfigContainer(application);
const bindsMount = generateBindMounts(mounts); const bindsMount = generateBindMounts(mounts);
@@ -191,6 +192,8 @@ export const mechanizeDockerContainer = async (
})), })),
}, },
UpdateConfig, UpdateConfig,
...(StopGracePeriod !== undefined &&
StopGracePeriod !== null && { StopGracePeriod }),
}; };
try { try {
@@ -220,8 +223,8 @@ const getImageName = (application: ApplicationNested) => {
if (registry) { if (registry) {
const { registryUrl, imagePrefix, username } = registry; const { registryUrl, imagePrefix, username } = registry;
const registryTag = imagePrefix const registryTag = imagePrefix
? `${registryUrl}/${imagePrefix}/${imageName}` ? `${registryUrl ? `${registryUrl}/` : ""}${imagePrefix}/${imageName}`
: `${registryUrl}/${username}/${imageName}`; : `${registryUrl ? `${registryUrl}/` : ""}${username}/${imageName}`;
return registryTag; return registryTag;
} }

View File

@@ -22,8 +22,8 @@ export const uploadImage = async (
// For ghcr.io: ghcr.io/username/image:tag // For ghcr.io: ghcr.io/username/image:tag
// For docker.io: docker.io/username/image:tag // For docker.io: docker.io/username/image:tag
const registryTag = imagePrefix const registryTag = imagePrefix
? `${registryUrl}/${imagePrefix}/${imageName}` ? `${registryUrl ? `${registryUrl}/` : ""}${imagePrefix}/${imageName}`
: `${registryUrl}/${username}/${imageName}`; : `${registryUrl ? `${registryUrl}/` : ""}${username}/${imageName}`;
try { try {
writeStream.write( writeStream.write(

View File

@@ -45,6 +45,7 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
RollbackConfig, RollbackConfig,
UpdateConfig, UpdateConfig,
Networks, Networks,
StopGracePeriod,
} = generateConfigContainer(mariadb); } = generateConfigContainer(mariadb);
const resources = calculateResources({ const resources = calculateResources({
memoryLimit, memoryLimit,
@@ -102,6 +103,8 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
: [], : [],
}, },
UpdateConfig, UpdateConfig,
...(StopGracePeriod !== undefined &&
StopGracePeriod !== null && { StopGracePeriod }),
}; };
try { try {
const service = docker.getService(appName); const service = docker.getService(appName);

View File

@@ -91,6 +91,7 @@ ${command ?? "wait $MONGOD_PID"}`;
RollbackConfig, RollbackConfig,
UpdateConfig, UpdateConfig,
Networks, Networks,
StopGracePeriod,
} = generateConfigContainer(mongo); } = generateConfigContainer(mongo);
const resources = calculateResources({ const resources = calculateResources({
@@ -155,6 +156,8 @@ ${command ?? "wait $MONGOD_PID"}`;
: [], : [],
}, },
UpdateConfig, UpdateConfig,
...(StopGracePeriod !== undefined &&
StopGracePeriod !== null && { StopGracePeriod }),
}; };
try { try {

View File

@@ -51,6 +51,7 @@ export const buildMysql = async (mysql: MysqlNested) => {
RollbackConfig, RollbackConfig,
UpdateConfig, UpdateConfig,
Networks, Networks,
StopGracePeriod,
} = generateConfigContainer(mysql); } = generateConfigContainer(mysql);
const resources = calculateResources({ const resources = calculateResources({
memoryLimit, memoryLimit,
@@ -108,6 +109,8 @@ export const buildMysql = async (mysql: MysqlNested) => {
: [], : [],
}, },
UpdateConfig, UpdateConfig,
...(StopGracePeriod !== undefined &&
StopGracePeriod !== null && { StopGracePeriod }),
}; };
try { try {
const service = docker.getService(appName); const service = docker.getService(appName);

View File

@@ -44,6 +44,7 @@ export const buildPostgres = async (postgres: PostgresNested) => {
RollbackConfig, RollbackConfig,
UpdateConfig, UpdateConfig,
Networks, Networks,
StopGracePeriod,
} = generateConfigContainer(postgres); } = generateConfigContainer(postgres);
const resources = calculateResources({ const resources = calculateResources({
memoryLimit, memoryLimit,
@@ -101,6 +102,8 @@ export const buildPostgres = async (postgres: PostgresNested) => {
: [], : [],
}, },
UpdateConfig, UpdateConfig,
...(StopGracePeriod !== undefined &&
StopGracePeriod !== null && { StopGracePeriod }),
}; };
try { try {
const service = docker.getService(appName); const service = docker.getService(appName);

View File

@@ -42,6 +42,7 @@ export const buildRedis = async (redis: RedisNested) => {
RollbackConfig, RollbackConfig,
UpdateConfig, UpdateConfig,
Networks, Networks,
StopGracePeriod,
} = generateConfigContainer(redis); } = generateConfigContainer(redis);
const resources = calculateResources({ const resources = calculateResources({
memoryLimit, memoryLimit,
@@ -98,6 +99,8 @@ export const buildRedis = async (redis: RedisNested) => {
: [], : [],
}, },
UpdateConfig, UpdateConfig,
...(StopGracePeriod !== undefined &&
StopGracePeriod !== null && { StopGracePeriod }),
}; };
try { try {

View File

@@ -1,5 +1,5 @@
import { findComposeById } from "@dokploy/server/services/compose"; import { findComposeById } from "@dokploy/server/services/compose";
import { dump } from "js-yaml"; import { stringify } from "yaml";
import { addAppNameToAllServiceNames } from "./collision/root-network"; import { addAppNameToAllServiceNames } from "./collision/root-network";
import { generateRandomHash } from "./compose"; import { generateRandomHash } from "./compose";
import { addSuffixToAllVolumes } from "./compose/volume"; import { addSuffixToAllVolumes } from "./compose/volume";
@@ -59,7 +59,7 @@ export const randomizeIsolatedDeploymentComposeFile = async (
) )
: composeData; : composeData;
return dump(newComposeFile); return stringify(newComposeFile);
}; };
export const randomizeDeployableSpecificationFile = ( export const randomizeDeployableSpecificationFile = (

Some files were not shown because too many files have changed in this diff Show More