mirror of
https://github.com/dokploy/dokploy.git
synced 2026-06-14 03:19:49 +00:00
Merge pull request #2863 from KarpachMarko/feature/custom-entrypoint
feat: add support for custom entry point
This commit is contained in:
@@ -32,6 +32,7 @@ describe("Host rule format regression tests", () => {
|
||||
previewDeploymentId: "",
|
||||
internalPath: "/",
|
||||
stripPath: false,
|
||||
customEntrypoint: null,
|
||||
};
|
||||
|
||||
describe("Host rule format validation", () => {
|
||||
|
||||
@@ -7,6 +7,7 @@ describe("createDomainLabels", () => {
|
||||
const baseDomain: Domain = {
|
||||
host: "example.com",
|
||||
port: 8080,
|
||||
customEntrypoint: null,
|
||||
https: false,
|
||||
uniqueConfigKey: 1,
|
||||
customCertResolver: null,
|
||||
@@ -240,4 +241,134 @@ describe("createDomainLabels", () => {
|
||||
"traefik.http.routers.test-app-1-websecure.middlewares=stripprefix-test-app-1,addprefix-test-app-1",
|
||||
);
|
||||
});
|
||||
|
||||
it("should create basic labels for custom entrypoint", async () => {
|
||||
const labels = await createDomainLabels(
|
||||
appName,
|
||||
{ ...baseDomain, customEntrypoint: "custom" },
|
||||
"custom",
|
||||
);
|
||||
expect(labels).toEqual([
|
||||
"traefik.http.routers.test-app-1-custom.rule=Host(`example.com`)",
|
||||
"traefik.http.routers.test-app-1-custom.entrypoints=custom",
|
||||
"traefik.http.services.test-app-1-custom.loadbalancer.server.port=8080",
|
||||
"traefik.http.routers.test-app-1-custom.service=test-app-1-custom",
|
||||
]);
|
||||
});
|
||||
|
||||
it("should create https labels for custom entrypoint", async () => {
|
||||
const labels = await createDomainLabels(
|
||||
appName,
|
||||
{
|
||||
...baseDomain,
|
||||
https: true,
|
||||
customEntrypoint: "custom",
|
||||
certificateType: "letsencrypt",
|
||||
},
|
||||
"custom",
|
||||
);
|
||||
expect(labels).toEqual([
|
||||
"traefik.http.routers.test-app-1-custom.rule=Host(`example.com`)",
|
||||
"traefik.http.routers.test-app-1-custom.entrypoints=custom",
|
||||
"traefik.http.services.test-app-1-custom.loadbalancer.server.port=8080",
|
||||
"traefik.http.routers.test-app-1-custom.service=test-app-1-custom",
|
||||
"traefik.http.routers.test-app-1-custom.tls.certresolver=letsencrypt",
|
||||
]);
|
||||
});
|
||||
|
||||
it("should add stripPath middleware for custom entrypoint", async () => {
|
||||
const labels = await createDomainLabels(
|
||||
appName,
|
||||
{
|
||||
...baseDomain,
|
||||
customEntrypoint: "custom",
|
||||
path: "/api",
|
||||
stripPath: true,
|
||||
},
|
||||
"custom",
|
||||
);
|
||||
|
||||
expect(labels).toContain(
|
||||
"traefik.http.middlewares.stripprefix-test-app-1.stripprefix.prefixes=/api",
|
||||
);
|
||||
expect(labels).toContain(
|
||||
"traefik.http.routers.test-app-1-custom.middlewares=stripprefix-test-app-1",
|
||||
);
|
||||
});
|
||||
|
||||
it("should add internalPath middleware for custom entrypoint", async () => {
|
||||
const labels = await createDomainLabels(
|
||||
appName,
|
||||
{
|
||||
...baseDomain,
|
||||
customEntrypoint: "custom",
|
||||
internalPath: "/hello",
|
||||
},
|
||||
"custom",
|
||||
);
|
||||
|
||||
expect(labels).toContain(
|
||||
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
|
||||
);
|
||||
expect(labels).toContain(
|
||||
"traefik.http.routers.test-app-1-custom.middlewares=addprefix-test-app-1",
|
||||
);
|
||||
});
|
||||
|
||||
it("should add path prefix in rule for custom entrypoint", async () => {
|
||||
const labels = await createDomainLabels(
|
||||
appName,
|
||||
{
|
||||
...baseDomain,
|
||||
customEntrypoint: "custom",
|
||||
path: "/api",
|
||||
},
|
||||
"custom",
|
||||
);
|
||||
|
||||
expect(labels).toContain(
|
||||
"traefik.http.routers.test-app-1-custom.rule=Host(`example.com`) && PathPrefix(`/api`)",
|
||||
);
|
||||
});
|
||||
|
||||
it("should combine all middlewares for custom entrypoint", async () => {
|
||||
const labels = await createDomainLabels(
|
||||
appName,
|
||||
{
|
||||
...baseDomain,
|
||||
customEntrypoint: "custom",
|
||||
path: "/api",
|
||||
stripPath: true,
|
||||
internalPath: "/hello",
|
||||
},
|
||||
"custom",
|
||||
);
|
||||
|
||||
expect(labels).toContain(
|
||||
"traefik.http.middlewares.stripprefix-test-app-1.stripprefix.prefixes=/api",
|
||||
);
|
||||
expect(labels).toContain(
|
||||
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
|
||||
);
|
||||
expect(labels).toContain(
|
||||
"traefik.http.routers.test-app-1-custom.middlewares=stripprefix-test-app-1,addprefix-test-app-1",
|
||||
);
|
||||
});
|
||||
|
||||
it("should not add redirect-to-https for custom entrypoint even with https", async () => {
|
||||
const labels = await createDomainLabels(
|
||||
appName,
|
||||
{
|
||||
...baseDomain,
|
||||
customEntrypoint: "custom",
|
||||
https: true,
|
||||
certificateType: "letsencrypt",
|
||||
},
|
||||
"custom",
|
||||
);
|
||||
|
||||
const middlewareLabel = labels.find((l) => l.includes(".middlewares="));
|
||||
// Should not contain redirect-to-https since there's only one router
|
||||
expect(middlewareLabel).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -137,6 +137,7 @@ const baseDomain: Domain = {
|
||||
https: false,
|
||||
path: null,
|
||||
port: null,
|
||||
customEntrypoint: null,
|
||||
serviceName: "",
|
||||
composeId: "",
|
||||
customCertResolver: null,
|
||||
@@ -276,6 +277,110 @@ test("CertificateType on websecure entrypoint", async () => {
|
||||
expect(router.tls?.certResolver).toBe("letsencrypt");
|
||||
});
|
||||
|
||||
test("Custom entrypoint on http domain", async () => {
|
||||
const router = await createRouterConfig(
|
||||
baseApp,
|
||||
{ ...baseDomain, https: false, customEntrypoint: "custom" },
|
||||
"custom",
|
||||
);
|
||||
|
||||
expect(router.entryPoints).toEqual(["custom"]);
|
||||
expect(router.middlewares).not.toContain("redirect-to-https");
|
||||
expect(router.tls).toBeUndefined();
|
||||
});
|
||||
|
||||
test("Custom entrypoint on https domain", async () => {
|
||||
const router = await createRouterConfig(
|
||||
baseApp,
|
||||
{
|
||||
...baseDomain,
|
||||
https: true,
|
||||
customEntrypoint: "custom",
|
||||
certificateType: "letsencrypt",
|
||||
},
|
||||
"custom",
|
||||
);
|
||||
|
||||
expect(router.entryPoints).toEqual(["custom"]);
|
||||
expect(router.middlewares).not.toContain("redirect-to-https");
|
||||
expect(router.tls?.certResolver).toBe("letsencrypt");
|
||||
});
|
||||
|
||||
test("Custom entrypoint with path includes PathPrefix in rule", async () => {
|
||||
const router = await createRouterConfig(
|
||||
baseApp,
|
||||
{ ...baseDomain, customEntrypoint: "custom", path: "/api" },
|
||||
"custom",
|
||||
);
|
||||
|
||||
expect(router.rule).toContain("PathPrefix(`/api`)");
|
||||
expect(router.entryPoints).toEqual(["custom"]);
|
||||
});
|
||||
|
||||
test("Custom entrypoint with stripPath adds stripprefix middleware", async () => {
|
||||
const router = await createRouterConfig(
|
||||
baseApp,
|
||||
{
|
||||
...baseDomain,
|
||||
customEntrypoint: "custom",
|
||||
path: "/api",
|
||||
stripPath: true,
|
||||
},
|
||||
"custom",
|
||||
);
|
||||
|
||||
expect(router.middlewares).toContain("stripprefix--1");
|
||||
expect(router.entryPoints).toEqual(["custom"]);
|
||||
});
|
||||
|
||||
test("Custom entrypoint with internalPath adds addprefix middleware", async () => {
|
||||
const router = await createRouterConfig(
|
||||
baseApp,
|
||||
{
|
||||
...baseDomain,
|
||||
customEntrypoint: "custom",
|
||||
internalPath: "/hello",
|
||||
},
|
||||
"custom",
|
||||
);
|
||||
|
||||
expect(router.middlewares).toContain("addprefix--1");
|
||||
expect(router.entryPoints).toEqual(["custom"]);
|
||||
});
|
||||
|
||||
test("Custom entrypoint with https and custom cert resolver", async () => {
|
||||
const router = await createRouterConfig(
|
||||
baseApp,
|
||||
{
|
||||
...baseDomain,
|
||||
https: true,
|
||||
customEntrypoint: "custom",
|
||||
certificateType: "custom",
|
||||
customCertResolver: "myresolver",
|
||||
},
|
||||
"custom",
|
||||
);
|
||||
|
||||
expect(router.entryPoints).toEqual(["custom"]);
|
||||
expect(router.tls?.certResolver).toBe("myresolver");
|
||||
});
|
||||
|
||||
test("Custom entrypoint without https should not have tls", async () => {
|
||||
const router = await createRouterConfig(
|
||||
baseApp,
|
||||
{
|
||||
...baseDomain,
|
||||
https: false,
|
||||
customEntrypoint: "custom",
|
||||
certificateType: "letsencrypt",
|
||||
},
|
||||
"custom",
|
||||
);
|
||||
|
||||
expect(router.entryPoints).toEqual(["custom"]);
|
||||
expect(router.tls).toBeUndefined();
|
||||
});
|
||||
|
||||
/** IDN/Punycode */
|
||||
|
||||
test("Internationalized domain name is converted to punycode", async () => {
|
||||
|
||||
@@ -61,6 +61,8 @@ export const domain = z
|
||||
.min(1, { message: "Port must be at least 1" })
|
||||
.max(65535, { message: "Port must be 65535 or below" })
|
||||
.optional(),
|
||||
useCustomEntrypoint: z.boolean(),
|
||||
customEntrypoint: z.string().optional(),
|
||||
https: z.boolean().optional(),
|
||||
certificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
|
||||
customCertResolver: z.string().optional(),
|
||||
@@ -114,6 +116,14 @@ export const domain = z
|
||||
message: "Internal path must start with '/'",
|
||||
});
|
||||
}
|
||||
|
||||
if (input.useCustomEntrypoint && !input.customEntrypoint) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["customEntrypoint"],
|
||||
message: "Custom entry point must be specified",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
type Domain = z.infer<typeof domain>;
|
||||
@@ -196,6 +206,8 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||
internalPath: undefined,
|
||||
stripPath: false,
|
||||
port: undefined,
|
||||
useCustomEntrypoint: false,
|
||||
customEntrypoint: undefined,
|
||||
https: false,
|
||||
certificateType: undefined,
|
||||
customCertResolver: undefined,
|
||||
@@ -206,6 +218,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||
});
|
||||
|
||||
const certificateType = form.watch("certificateType");
|
||||
const useCustomEntrypoint = form.watch("useCustomEntrypoint");
|
||||
const https = form.watch("https");
|
||||
const domainType = form.watch("domainType");
|
||||
const host = form.watch("host");
|
||||
@@ -220,6 +233,8 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||
internalPath: data?.internalPath || undefined,
|
||||
stripPath: data?.stripPath || false,
|
||||
port: data?.port || undefined,
|
||||
useCustomEntrypoint: !!data.customEntrypoint,
|
||||
customEntrypoint: data.customEntrypoint || undefined,
|
||||
certificateType: data?.certificateType || undefined,
|
||||
customCertResolver: data?.customCertResolver || undefined,
|
||||
serviceName: data?.serviceName || undefined,
|
||||
@@ -234,6 +249,8 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||
internalPath: undefined,
|
||||
stripPath: false,
|
||||
port: undefined,
|
||||
useCustomEntrypoint: false,
|
||||
customEntrypoint: undefined,
|
||||
https: false,
|
||||
certificateType: undefined,
|
||||
customCertResolver: undefined,
|
||||
@@ -635,6 +652,50 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="useCustomEntrypoint"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Custom Entrypoint</FormLabel>
|
||||
<FormDescription>
|
||||
Use custom entrypoint for domina
|
||||
<br />
|
||||
"web" and/or "websecure" is used by default.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{useCustomEntrypoint && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="customEntrypoint"
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormLabel>Entrypoint Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Enter entrypoint name manually"
|
||||
{...field}
|
||||
className="w-full"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="https"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE "domain" ADD COLUMN "customEntrypoint" text;
|
||||
@@ -31,6 +31,7 @@ export const domains = pgTable("domain", {
|
||||
host: text("host").notNull(),
|
||||
https: boolean("https").notNull().default(false),
|
||||
port: integer("port").default(3000),
|
||||
customEntrypoint: text("customEntrypoint"),
|
||||
path: text("path").default("/"),
|
||||
serviceName: text("serviceName"),
|
||||
domainType: domainType("domainType").default("application"),
|
||||
@@ -80,6 +81,7 @@ export const apiCreateDomain = createSchema.pick({
|
||||
host: true,
|
||||
path: true,
|
||||
port: true,
|
||||
customEntrypoint: true,
|
||||
https: true,
|
||||
applicationId: true,
|
||||
certificateType: true,
|
||||
@@ -113,6 +115,7 @@ export const apiUpdateDomain = createSchema
|
||||
host: true,
|
||||
path: true,
|
||||
port: true,
|
||||
customEntrypoint: true,
|
||||
https: true,
|
||||
certificateType: true,
|
||||
customCertResolver: true,
|
||||
|
||||
@@ -172,8 +172,12 @@ export const addDomainToCompose = async (
|
||||
);
|
||||
}
|
||||
|
||||
const httpLabels = createDomainLabels(appName, domain, "web");
|
||||
if (https) {
|
||||
const httpLabels = createDomainLabels(
|
||||
appName,
|
||||
domain,
|
||||
domain.customEntrypoint || "web",
|
||||
);
|
||||
if (!domain.customEntrypoint && https) {
|
||||
const httpsLabels = createDomainLabels(appName, domain, "websecure");
|
||||
httpLabels.push(...httpsLabels);
|
||||
}
|
||||
@@ -251,11 +255,12 @@ export const writeComposeFile = async (
|
||||
export const createDomainLabels = (
|
||||
appName: string,
|
||||
domain: Domain,
|
||||
entrypoint: "web" | "websecure",
|
||||
entrypoint: string,
|
||||
) => {
|
||||
const {
|
||||
host,
|
||||
port,
|
||||
customEntrypoint,
|
||||
https,
|
||||
uniqueConfigKey,
|
||||
certificateType,
|
||||
@@ -284,7 +289,7 @@ export const createDomainLabels = (
|
||||
if (stripPath && path && path !== "/") {
|
||||
const middlewareName = `stripprefix-${appName}-${uniqueConfigKey}`;
|
||||
// Only define middleware once (on web entrypoint)
|
||||
if (entrypoint === "web") {
|
||||
if (entrypoint === "web" || customEntrypoint) {
|
||||
labels.push(
|
||||
`traefik.http.middlewares.${middlewareName}.stripprefix.prefixes=${path}`,
|
||||
);
|
||||
@@ -296,7 +301,7 @@ export const createDomainLabels = (
|
||||
if (internalPath && internalPath !== "/" && internalPath.startsWith("/")) {
|
||||
const middlewareName = `addprefix-${appName}-${uniqueConfigKey}`;
|
||||
// Only define middleware once (on web entrypoint)
|
||||
if (entrypoint === "web") {
|
||||
if (entrypoint === "web" || customEntrypoint) {
|
||||
labels.push(
|
||||
`traefik.http.middlewares.${middlewareName}.addprefix.prefix=${internalPath}`,
|
||||
);
|
||||
@@ -312,7 +317,7 @@ export const createDomainLabels = (
|
||||
}
|
||||
|
||||
// Add TLS configuration for websecure
|
||||
if (entrypoint === "websecure") {
|
||||
if (entrypoint === "websecure" || (customEntrypoint && https)) {
|
||||
if (certificateType === "letsencrypt") {
|
||||
labels.push(
|
||||
`traefik.http.routers.${routerName}.tls.certresolver=letsencrypt`,
|
||||
|
||||
@@ -32,10 +32,10 @@ export const manageDomain = async (app: ApplicationNested, domain: Domain) => {
|
||||
config.http.routers[routerName] = await createRouterConfig(
|
||||
app,
|
||||
domain,
|
||||
"web",
|
||||
domain.customEntrypoint || "web",
|
||||
);
|
||||
|
||||
if (domain.https) {
|
||||
if (!domain.customEntrypoint && domain.https) {
|
||||
config.http.routers[routerNameSecure] = await createRouterConfig(
|
||||
app,
|
||||
domain,
|
||||
@@ -121,13 +121,20 @@ const toPunycode = (host: string): string => {
|
||||
export const createRouterConfig = async (
|
||||
app: ApplicationNested,
|
||||
domain: Domain,
|
||||
entryPoint: "web" | "websecure",
|
||||
entryPoint: string,
|
||||
) => {
|
||||
const { appName, redirects, security } = app;
|
||||
const { certificateType } = domain;
|
||||
|
||||
const { host, path, https, uniqueConfigKey, internalPath, stripPath } =
|
||||
domain;
|
||||
const {
|
||||
host,
|
||||
path,
|
||||
https,
|
||||
uniqueConfigKey,
|
||||
internalPath,
|
||||
stripPath,
|
||||
customEntrypoint,
|
||||
} = domain;
|
||||
const punycodeHost = toPunycode(host);
|
||||
const routerConfig: HttpRouter = {
|
||||
rule: `Host(\`${punycodeHost}\`)${path !== null && path !== "/" ? ` && PathPrefix(\`${path}\`)` : ""}`,
|
||||
@@ -174,7 +181,7 @@ export const createRouterConfig = async (
|
||||
}
|
||||
}
|
||||
|
||||
if (entryPoint === "websecure") {
|
||||
if (entryPoint === "websecure" || (customEntrypoint && https)) {
|
||||
if (certificateType === "letsencrypt") {
|
||||
routerConfig.tls = { certResolver: "letsencrypt" };
|
||||
} else if (certificateType === "custom" && domain.customCertResolver) {
|
||||
|
||||
Reference in New Issue
Block a user