Compare commits

...

17 Commits

Author SHA1 Message Date
Mauricio Siu
47db6831b4 Merge pull request #1461 from Dokploy/fix/envs-array-templates
feat(templates): support array-based environment variable configuration
2025-03-11 00:42:03 -06:00
Mauricio Siu
56cbd1abb3 test(templates): enhance secret key and base64 validation in template tests
Improve test coverage for secret key generation by:
- Adding more robust base64 validation checks
- Verifying base64 string format and length
- Ensuring generated keys meet specific cryptographic requirements
2025-03-11 00:41:53 -06:00
Mauricio Siu
cb40ac5c6b Merge branch 'canary' into fix/envs-array-templates 2025-03-11 00:38:50 -06:00
Mauricio Siu
7218b3f79b feat(templates): support array-based environment variable configuration
Add support for processing environment variables defined as an array in template configurations, allowing more flexible env var definitions with direct string values and variable interpolation
2025-03-11 00:38:10 -06:00
Mauricio Siu
19ea4d3fcd Merge pull request #1459 from Dokploy/fix/tweak-processor-template
refactor: update project name reference in compose template processing
2025-03-11 00:29:33 -06:00
Mauricio Siu
6edfd1e547 Merge branch 'canary' into fix/tweak-processor-template 2025-03-11 00:29:26 -06:00
Mauricio Siu
666a8ede97 chore(version): bump project version to v0.20.2
Update package.json version to reflect minor release
2025-03-11 00:29:07 -06:00
Mauricio Siu
08e4b8fe33 refactor: update project name reference in compose template processing
Change references from `compose.project.name` to `compose.appName` when processing compose templates to ensure correct project naming
2025-03-11 00:27:59 -06:00
Mauricio Siu
5fc265d14f Merge pull request #1458 from nktnet1/fix-domain-overflow
fix: truncate domain overflow for external links
2025-03-11 00:24:11 -06:00
Khiet Tam Nguyen
c3887af5d1 fix: truncate domain overflow for external links 2025-03-11 12:42:21 +11:00
Mauricio Siu
a6684af57e fix(templates): add null checks for template config properties
Prevent potential runtime errors by adding null checks for domains, env, and mounts in template processors
2025-03-10 03:25:04 -06:00
Mauricio Siu
8df2b20c3b Merge branch 'main' into canary 2025-03-10 02:41:10 -06:00
Mauricio Siu
f159dc11eb fix(traefik): increase service removal wait time to 15 seconds
Extend the timeout duration when removing Traefik service to ensure complete service removal and prevent potential initialization issues
2025-03-10 02:23:17 -06:00
Mauricio Siu
fce22ec1d0 fix(traefik): increase migration wait time for service removal
Adjust sleep/timeout duration in Traefik migration scripts to ensure proper service removal and container initialization
2025-03-10 01:54:25 -06:00
Mauricio Siu
e63eed57dd refactor: remove throw 2025-03-10 01:49:00 -06:00
Mauricio Siu
acc8ce80ad fix(backups): prevent error propagation in backup cleanup
Remove unnecessary error throwing in backup cleanup to allow partial success and logging
2025-03-10 01:48:28 -06:00
Mauricio Siu
e317772367 Merge pull request #1452 from Dokploy/canary
🚀 Release v0.20.0
2025-03-10 01:30:25 -06:00
9 changed files with 125 additions and 31 deletions

View File

@@ -166,7 +166,38 @@ describe("processTemplate", () => {
if (!baseUrl || !secretKey) return;
expect(baseUrl).toContain(mockSchema.projectName);
expect(secretKey.split("=")[1]).toHaveLength(64);
const base64Value = secretKey.split("=")[1];
expect(base64Value).toBeDefined();
if (!base64Value) return;
expect(base64Value).toMatch(/^[A-Za-z0-9+/]+={0,2}$/);
expect(base64Value.length).toBeGreaterThanOrEqual(86);
expect(base64Value.length).toBeLessThanOrEqual(88);
});
it("should process env vars when provided as an array", () => {
const template: CompleteTemplate = {
metadata: {} as any,
variables: {},
config: {
domains: [],
env: [
'CLOUDFLARE_TUNNEL_TOKEN="<INSERT TOKEN>"',
'ANOTHER_VAR="some value"',
"DOMAIN=${domain}",
],
mounts: [],
},
};
const result = processTemplate(template, mockSchema);
expect(result.envs).toHaveLength(3);
// Should preserve exact format for static values
expect(result.envs[0]).toBe('CLOUDFLARE_TUNNEL_TOKEN="<INSERT TOKEN>"');
expect(result.envs[1]).toBe('ANOTHER_VAR="some value"');
// Should process variables in array items
expect(result.envs[2]).toContain(mockSchema.projectName);
});
it("should allow using utility functions directly in env vars", () => {
@@ -195,7 +226,12 @@ describe("processTemplate", () => {
if (!randomDomainEnv || !secretKeyEnv) return;
expect(randomDomainEnv).toContain(mockSchema.projectName);
expect(secretKeyEnv.split("=")[1]).toHaveLength(32);
const base64Value = secretKeyEnv.split("=")[1];
expect(base64Value).toBeDefined();
if (!base64Value) return;
expect(base64Value).toMatch(/^[A-Za-z0-9+/]+={0,2}$/);
expect(base64Value.length).toBeGreaterThanOrEqual(42);
expect(base64Value.length).toBeLessThanOrEqual(44);
});
});
@@ -325,8 +361,22 @@ describe("processTemplate", () => {
if (!baseUrl || !secretKey || !totpKey) return;
expect(baseUrl).toContain(mockSchema.projectName);
expect(secretKey.split("=")[1]).toHaveLength(64);
expect(totpKey.split("=")[1]).toHaveLength(32);
// Check base64 lengths and format
const secretKeyValue = secretKey.split("=")[1];
const totpKeyValue = totpKey.split("=")[1];
expect(secretKeyValue).toBeDefined();
expect(totpKeyValue).toBeDefined();
if (!secretKeyValue || !totpKeyValue) return;
expect(secretKeyValue).toMatch(/^[A-Za-z0-9+/]+={0,2}$/);
expect(secretKeyValue.length).toBeGreaterThanOrEqual(86);
expect(secretKeyValue.length).toBeLessThanOrEqual(88);
expect(totpKeyValue).toMatch(/^[A-Za-z0-9+/]+={0,2}$/);
expect(totpKeyValue.length).toBeGreaterThanOrEqual(42);
expect(totpKeyValue.length).toBeLessThanOrEqual(44);
// Check mounts
expect(result.mounts).toHaveLength(1);
@@ -334,8 +384,8 @@ describe("processTemplate", () => {
expect(mount).toBeDefined();
if (!mount) return;
expect(mount.content).toContain(mockSchema.projectName);
expect(mount.content).toMatch(/secret=[A-Za-z0-9+/]{64}/);
expect(mount.content).toMatch(/totp=[A-Za-z0-9+/]{32}/);
expect(mount.content).toMatch(/secret=[A-Za-z0-9+/]{86,88}/);
expect(mount.content).toMatch(/totp=[A-Za-z0-9+/]{42,44}/);
});
});

View File

@@ -186,7 +186,7 @@ export const ShowProjects = () => {
target="_blank"
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
>
<span>{domain.host}</span>
<span className="truncate">{domain.host}</span>
<ExternalLinkIcon className="size-4 shrink-0" />
</Link>
</DropdownMenuItem>
@@ -222,7 +222,7 @@ export const ShowProjects = () => {
target="_blank"
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
>
<span>{domain.host}</span>
<span className="truncate">{domain.host}</span>
<ExternalLinkIcon className="size-4 shrink-0" />
</Link>
</DropdownMenuItem>

View File

@@ -1,6 +1,6 @@
{
"name": "dokploy",
"version": "v0.20.0",
"version": "v0.20.2",
"private": true,
"license": "Apache-2.0",
"type": "module",

View File

@@ -607,7 +607,7 @@ export const composeRouter = createTRPCRouter({
const processedTemplate = processTemplate(config, {
serverIp: serverIp,
projectName: compose.project.name,
projectName: compose.appName,
});
return {
@@ -676,7 +676,7 @@ export const composeRouter = createTRPCRouter({
const processedTemplate = processTemplate(config, {
serverIp: serverIp,
projectName: compose.project.name,
projectName: compose.appName,
});
// Update compose file

View File

@@ -546,7 +546,7 @@ export const createTraefikInstance = () => {
if docker service inspect dokploy-traefik > /dev/null 2>&1; then
echo "Migrating Traefik to Standalone..."
docker service rm dokploy-traefik
sleep 7
sleep 8
echo "Traefik migrated to Standalone ✅"
fi

View File

@@ -91,9 +91,26 @@ export const initializeTraefik = async ({
try {
const service = docker.getService("dokploy-traefik");
await service?.remove({ force: true });
await new Promise((resolve) => setTimeout(resolve, 5000));
} catch (_) {}
let attempts = 0;
const maxAttempts = 5;
while (attempts < maxAttempts) {
try {
await docker.listServices({
filters: { name: ["dokploy-traefik"] },
});
console.log("Waiting for service cleanup...");
await new Promise((resolve) => setTimeout(resolve, 5000));
attempts++;
} catch (e) {
break;
}
}
} catch (err) {
console.log("No existing service to remove");
}
// Then try to remove any existing container
const container = docker.getContainer(containerName);
try {
const inspect = await container.inspect();
@@ -103,15 +120,31 @@ export const initializeTraefik = async ({
}
await container.remove({ force: true });
await new Promise((resolve) => setTimeout(resolve, 5000));
} catch (error) {
console.log(error);
console.log("No existing container to remove");
}
await docker.createContainer(settings);
const newContainer = docker.getContainer(containerName);
await newContainer.start();
// Create and start the new container
try {
await docker.createContainer(settings);
const newContainer = docker.getContainer(containerName);
await newContainer.start();
console.log("Traefik container started successfully");
} catch (error: any) {
if (error?.json?.message?.includes("port is already allocated")) {
console.log("Ports still in use, waiting longer for cleanup...");
await new Promise((resolve) => setTimeout(resolve, 10000));
// Try one more time
await docker.createContainer(settings);
const newContainer = docker.getContainer(containerName);
await newContainer.start();
console.log("Traefik container started successfully after retry");
}
}
} catch (error) {
console.log(error);
console.error("Failed to initialize Traefik:", error);
throw error;
}
};

View File

@@ -53,14 +53,12 @@ export const generatePassword = (quantity = 16): string => {
};
/**
* Generate a random base64 string of specified length
* Generate a random base64 string from N random bytes
* @param bytes Number of random bytes to generate before base64 encoding (default: 32)
* @returns base64 encoded string of the random bytes
*/
export function generateBase64(length: number): string {
// To get N characters in base64, we need to generate N * 3/4 bytes
const bytesNeeded = Math.ceil((length * 3) / 4);
return Buffer.from(randomBytes(bytesNeeded))
.toString("base64")
.substring(0, length);
export function generateBase64(bytes = 32): string {
return randomBytes(bytes).toString("base64");
}
export function generateJwt(length = 256): string {

View File

@@ -45,7 +45,7 @@ export interface CompleteTemplate {
variables: Record<string, string>;
config: {
domains: DomainConfig[];
env: Record<string, string>;
env: Record<string, string> | string[];
mounts?: MountConfig[];
};
}
@@ -175,7 +175,8 @@ export function processDomains(
variables: Record<string, string>,
schema: Schema,
): Template["domains"] {
return template.config.domains.map((domain: DomainConfig) => ({
if (!template?.config?.domains) return [];
return template?.config?.domains?.map((domain: DomainConfig) => ({
...domain,
host: domain.host
? processValue(domain.host, variables, schema)
@@ -191,6 +192,19 @@ export function processEnvVars(
variables: Record<string, string>,
schema: Schema,
): Template["envs"] {
if (!template?.config?.env) return [];
// Handle array of env vars
if (Array.isArray(template.config.env)) {
return template.config.env.map((env) => {
if (typeof env === "string") {
return processValue(env, variables, schema);
}
return env;
});
}
// Handle object of env vars
return Object.entries(template.config.env).map(
([key, value]: [string, string]) => {
const processedValue = processValue(value, variables, schema);
@@ -207,9 +221,9 @@ export function processMounts(
variables: Record<string, string>,
schema: Schema,
): Template["mounts"] {
if (!template.config.mounts) return [];
if (!template?.config?.mounts) return [];
return template.config.mounts.map((mount: MountConfig) => ({
return template?.config?.mounts?.map((mount: MountConfig) => ({
filePath: processValue(mount.filePath, variables, schema),
content: processValue(mount.content, variables, schema),
}));

View File

@@ -212,6 +212,5 @@ export const keepLatestNBackups = async (
}
} catch (error) {
console.error(error);
throw error;
}
};