mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-09-14 11:06:15 +05:00
Merge pull request #5202 from Dokploy/fix/traefik-empty-dynamic-config-5189
fix: don't write invalid empty traefik config for apps without domains
This commit is contained in:
commit
203cddbeef
104
apps/dokploy/__test__/traefik/write-app-traefik-config.test.ts
Normal file
104
apps/dokploy/__test__/traefik/write-app-traefik-config.test.ts
Normal file
@ -0,0 +1,104 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { writeAppTraefikConfig } from "@dokploy/server/utils/traefik/application";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
execAsyncRemote: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@dokploy/server/utils/process/execAsync", async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import("@dokploy/server/utils/process/execAsync")>();
|
||||
return {
|
||||
...actual,
|
||||
execAsyncRemote: mocks.execAsyncRemote,
|
||||
};
|
||||
});
|
||||
|
||||
describe("writeAppTraefikConfig", () => {
|
||||
let cwd: string;
|
||||
let dynamicPath: string;
|
||||
|
||||
beforeEach(() => {
|
||||
cwd = fs.mkdtempSync(path.join(os.tmpdir(), "dokploy-traefik-"));
|
||||
dynamicPath = path.join(cwd, ".docker", "traefik", "dynamic");
|
||||
fs.mkdirSync(dynamicPath, { recursive: true });
|
||||
vi.spyOn(process, "cwd").mockReturnValue(cwd);
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
fs.rmSync(cwd, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
// Regression test for #5189: Traefik's file provider rejects a standalone
|
||||
// `routers: {}` / `services: {}` map and aborts its watcher for every
|
||||
// dynamic config once it hits one, so an app with no domains must never
|
||||
// get an on-disk config file at all.
|
||||
it("removes the file instead of writing empty routers/services", async () => {
|
||||
const appName = "no-domain-app";
|
||||
const configPath = path.join(dynamicPath, `${appName}.yml`);
|
||||
fs.writeFileSync(configPath, "stale content", "utf8");
|
||||
|
||||
await writeAppTraefikConfig(
|
||||
{ http: { routers: {}, services: {} } },
|
||||
appName,
|
||||
);
|
||||
|
||||
expect(fs.existsSync(configPath)).toBe(false);
|
||||
});
|
||||
|
||||
it("writes the file when routers/services are present", async () => {
|
||||
const appName = "with-domain-app";
|
||||
const configPath = path.join(dynamicPath, `${appName}.yml`);
|
||||
|
||||
await writeAppTraefikConfig(
|
||||
{
|
||||
http: {
|
||||
routers: { [`${appName}-router-1`]: { rule: "Host(`x`)" } },
|
||||
services: {},
|
||||
},
|
||||
},
|
||||
appName,
|
||||
);
|
||||
|
||||
expect(fs.existsSync(configPath)).toBe(true);
|
||||
});
|
||||
|
||||
it("removes the remote file instead of writing empty routers/services", async () => {
|
||||
mocks.execAsyncRemote.mockResolvedValue({ stdout: "", stderr: "" });
|
||||
|
||||
await writeAppTraefikConfig(
|
||||
{ http: { routers: {}, services: {} } },
|
||||
"no-domain-app",
|
||||
"server-id",
|
||||
);
|
||||
|
||||
expect(mocks.execAsyncRemote).toHaveBeenCalledOnce();
|
||||
const [, command] = mocks.execAsyncRemote.mock.calls[0];
|
||||
expect(command).toMatch(/^rm -f /);
|
||||
expect(command).toContain("no-domain-app.yml");
|
||||
});
|
||||
|
||||
it("writes the remote file when routers/services are present", async () => {
|
||||
mocks.execAsyncRemote.mockResolvedValue({ stdout: "", stderr: "" });
|
||||
|
||||
await writeAppTraefikConfig(
|
||||
{
|
||||
http: {
|
||||
routers: { "with-domain-app-router-1": { rule: "Host(`x`)" } },
|
||||
services: {},
|
||||
},
|
||||
},
|
||||
"with-domain-app",
|
||||
"server-id",
|
||||
);
|
||||
|
||||
expect(mocks.execAsyncRemote).toHaveBeenCalledOnce();
|
||||
const [, command] = mocks.execAsyncRemote.mock.calls[0];
|
||||
expect(command).toMatch(/^echo /);
|
||||
});
|
||||
});
|
||||
@ -292,6 +292,26 @@ export const writeTraefikConfigRemote = async (
|
||||
}
|
||||
};
|
||||
|
||||
const isEmptyHttpRoutersAndServices = (traefikConfig: FileConfig) =>
|
||||
Object.keys(traefikConfig.http?.routers || {}).length === 0 &&
|
||||
Object.keys(traefikConfig.http?.services || {}).length === 0;
|
||||
|
||||
export const writeAppTraefikConfig = async (
|
||||
traefikConfig: FileConfig,
|
||||
appName: string,
|
||||
serverId?: string | null,
|
||||
) => {
|
||||
if (isEmptyHttpRoutersAndServices(traefikConfig)) {
|
||||
await removeTraefikConfig(appName, serverId);
|
||||
return;
|
||||
}
|
||||
if (serverId) {
|
||||
await writeTraefikConfigRemote(traefikConfig, appName, serverId);
|
||||
} else {
|
||||
writeTraefikConfig(traefikConfig, appName);
|
||||
}
|
||||
};
|
||||
|
||||
export const createServiceConfig = (
|
||||
appName: string,
|
||||
domain: Domain,
|
||||
|
||||
@ -3,7 +3,7 @@ import type { ApplicationNested } from "../builders";
|
||||
import {
|
||||
loadOrCreateConfig,
|
||||
loadOrCreateConfigRemote,
|
||||
writeTraefikConfig,
|
||||
writeAppTraefikConfig,
|
||||
writeTraefikConfigRemote,
|
||||
} from "./application";
|
||||
import type { FileConfig } from "./file-types";
|
||||
@ -89,11 +89,10 @@ export const createRedirectMiddleware = async (
|
||||
|
||||
if (serverId) {
|
||||
await writeTraefikConfigRemote(config, "middlewares", serverId);
|
||||
await writeTraefikConfigRemote(appConfig, appName, serverId);
|
||||
} else {
|
||||
writeMiddleware(config);
|
||||
writeTraefikConfig(appConfig, appName);
|
||||
}
|
||||
await writeAppTraefikConfig(appConfig, appName, serverId);
|
||||
};
|
||||
|
||||
export const removeRedirectMiddleware = async (
|
||||
@ -124,9 +123,8 @@ export const removeRedirectMiddleware = async (
|
||||
|
||||
if (serverId) {
|
||||
await writeTraefikConfigRemote(config, "middlewares", serverId);
|
||||
await writeTraefikConfigRemote(appConfig, appName, serverId);
|
||||
} else {
|
||||
writeTraefikConfig(appConfig, appName);
|
||||
writeMiddleware(config);
|
||||
}
|
||||
await writeAppTraefikConfig(appConfig, appName, serverId);
|
||||
};
|
||||
|
||||
@ -4,7 +4,7 @@ import type { ApplicationNested } from "../builders";
|
||||
import {
|
||||
loadOrCreateConfig,
|
||||
loadOrCreateConfigRemote,
|
||||
writeTraefikConfig,
|
||||
writeAppTraefikConfig,
|
||||
writeTraefikConfigRemote,
|
||||
} from "./application";
|
||||
import type {
|
||||
@ -62,11 +62,10 @@ export const createSecurityMiddleware = async (
|
||||
addMiddleware(appConfig, middlewareName);
|
||||
if (serverId) {
|
||||
await writeTraefikConfigRemote(config, "middlewares", serverId);
|
||||
await writeTraefikConfigRemote(appConfig, appName, serverId);
|
||||
} else {
|
||||
writeTraefikConfig(appConfig, appName);
|
||||
writeMiddleware(config);
|
||||
}
|
||||
await writeAppTraefikConfig(appConfig, appName, serverId);
|
||||
};
|
||||
|
||||
export const removeSecurityMiddleware = async (
|
||||
@ -89,6 +88,7 @@ export const removeSecurityMiddleware = async (
|
||||
appConfig = loadOrCreateConfig(appName);
|
||||
}
|
||||
const middlewareName = `auth-${appName}`;
|
||||
let removedLastUser = false;
|
||||
|
||||
if (config.http?.middlewares) {
|
||||
const currentMiddleware = config.http.middlewares[middlewareName];
|
||||
@ -106,11 +106,7 @@ export const removeSecurityMiddleware = async (
|
||||
delete config.http.middlewares[middlewareName];
|
||||
}
|
||||
deleteMiddleware(appConfig, middlewareName);
|
||||
if (serverId) {
|
||||
await writeTraefikConfigRemote(appConfig, appName, serverId);
|
||||
} else {
|
||||
writeTraefikConfig(appConfig, appName);
|
||||
}
|
||||
removedLastUser = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -120,6 +116,9 @@ export const removeSecurityMiddleware = async (
|
||||
} else {
|
||||
writeMiddleware(config);
|
||||
}
|
||||
if (removedLastUser) {
|
||||
await writeAppTraefikConfig(appConfig, appName, serverId);
|
||||
}
|
||||
};
|
||||
|
||||
const isBasicAuthMiddleware = (
|
||||
|
||||
Loading…
Reference in New Issue
Block a user