mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-16 12:45:21 +02:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85632fd0c2 | ||
|
|
31cdae1b72 | ||
|
|
702af64444 | ||
|
|
eef27b67c2 | ||
|
|
70f50dd8bc | ||
|
|
3e25b97b99 | ||
|
|
22927c2716 | ||
|
|
8ab4ee8e0e | ||
|
|
99aa34f27e | ||
|
|
48be8544cf | ||
|
|
ee411ac74f | ||
|
|
c233ddb520 | ||
|
|
0cfe87cb72 | ||
|
|
7998b296a2 | ||
|
|
9e20f66bf5 | ||
|
|
1dc943ef5b | ||
|
|
0f63fdac4e | ||
|
|
ec8c516aa3 | ||
|
|
58be8f91c0 | ||
|
|
2036ac3dc8 | ||
|
|
17f83f746a | ||
|
|
bcd1cbe920 | ||
|
|
3993263615 | ||
|
|
97bd4de4f1 | ||
|
|
2fc29ff7c8 | ||
|
|
4a74016b52 |
209
apps/dokploy/__test__/cluster/upload.test.ts
Normal file
209
apps/dokploy/__test__/cluster/upload.test.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
import type { Registry } from "@dokploy/server";
|
||||
import { getRegistryTag } from "@dokploy/server";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("getRegistryTag", () => {
|
||||
// Helper to create a mock registry
|
||||
const createMockRegistry = (overrides: Partial<Registry> = {}): Registry => {
|
||||
return {
|
||||
registryId: "test-registry-id",
|
||||
registryName: "Test Registry",
|
||||
username: "myuser",
|
||||
password: "test-password",
|
||||
registryUrl: "docker.io",
|
||||
registryType: "cloud",
|
||||
imagePrefix: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
organizationId: "test-org-id",
|
||||
...overrides,
|
||||
};
|
||||
};
|
||||
|
||||
describe("with username (no imagePrefix)", () => {
|
||||
it("should handle simple image name without tag", () => {
|
||||
const registry = createMockRegistry({ username: "myuser" });
|
||||
const result = getRegistryTag(registry, "nginx");
|
||||
expect(result).toBe("docker.io/myuser/nginx");
|
||||
});
|
||||
|
||||
it("should handle image name with tag", () => {
|
||||
const registry = createMockRegistry({ username: "myuser" });
|
||||
const result = getRegistryTag(registry, "nginx:latest");
|
||||
expect(result).toBe("docker.io/myuser/nginx:latest");
|
||||
});
|
||||
|
||||
it("should handle image name with username already present (no duplication)", () => {
|
||||
const registry = createMockRegistry({ username: "myuser" });
|
||||
const result = getRegistryTag(registry, "myuser/myprivaterepo");
|
||||
// Should not duplicate username
|
||||
expect(result).toBe("docker.io/myuser/myprivaterepo");
|
||||
});
|
||||
|
||||
it("should handle image name with username and tag already present", () => {
|
||||
const registry = createMockRegistry({ username: "myuser" });
|
||||
const result = getRegistryTag(registry, "myuser/myprivaterepo:latest");
|
||||
// Should not duplicate username
|
||||
expect(result).toBe("docker.io/myuser/myprivaterepo:latest");
|
||||
});
|
||||
|
||||
it("should handle complex image name with username", () => {
|
||||
const registry = createMockRegistry({ username: "siumauricio" });
|
||||
const result = getRegistryTag(
|
||||
registry,
|
||||
"siumauricio/app-parse-multi-byte-port-e32uh7",
|
||||
);
|
||||
// Should not duplicate username
|
||||
expect(result).toBe(
|
||||
"docker.io/siumauricio/app-parse-multi-byte-port-e32uh7",
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle image name with different username (should not duplicate)", () => {
|
||||
const registry = createMockRegistry({ username: "myuser" });
|
||||
const result = getRegistryTag(registry, "otheruser/myprivaterepo");
|
||||
expect(result).toBe("docker.io/myuser/myprivaterepo");
|
||||
});
|
||||
|
||||
it("should handle image name with full registry URL (no username)", () => {
|
||||
const registry = createMockRegistry({ username: "myuser" });
|
||||
const result = getRegistryTag(registry, "docker.io/nginx");
|
||||
// Should add username since imageName doesn't have one
|
||||
expect(result).toBe("docker.io/myuser/nginx");
|
||||
});
|
||||
|
||||
it("should handle image name with custom registry URL and username", () => {
|
||||
const registry = createMockRegistry({ username: "myuser" });
|
||||
const result = getRegistryTag(registry, "ghcr.io/myuser/repo");
|
||||
// Should not duplicate username even if registry URL is different
|
||||
expect(result).toBe("docker.io/myuser/repo");
|
||||
});
|
||||
|
||||
it("should handle image name with custom registry URL (different username)", () => {
|
||||
const registry = createMockRegistry({ username: "myuser" });
|
||||
const result = getRegistryTag(registry, "ghcr.io/otheruser/repo");
|
||||
// Should use registry username, not the one in imageName
|
||||
expect(result).toBe("docker.io/myuser/repo");
|
||||
});
|
||||
});
|
||||
|
||||
describe("with imagePrefix", () => {
|
||||
it("should use imagePrefix instead of username", () => {
|
||||
const registry = createMockRegistry({
|
||||
username: "myuser",
|
||||
imagePrefix: "myorg",
|
||||
});
|
||||
const result = getRegistryTag(registry, "nginx");
|
||||
expect(result).toBe("docker.io/myorg/nginx");
|
||||
});
|
||||
|
||||
it("should use imagePrefix with image tag", () => {
|
||||
const registry = createMockRegistry({
|
||||
username: "myuser",
|
||||
imagePrefix: "myorg",
|
||||
});
|
||||
const result = getRegistryTag(registry, "nginx:latest");
|
||||
expect(result).toBe("docker.io/myorg/nginx:latest");
|
||||
});
|
||||
|
||||
it("should handle imagePrefix with username already in image name", () => {
|
||||
const registry = createMockRegistry({
|
||||
username: "myuser",
|
||||
imagePrefix: "myorg",
|
||||
});
|
||||
const result = getRegistryTag(registry, "myuser/myprivaterepo");
|
||||
expect(result).toBe("docker.io/myorg/myprivaterepo");
|
||||
});
|
||||
|
||||
it("should handle imagePrefix matching image name prefix", () => {
|
||||
const registry = createMockRegistry({
|
||||
username: "myuser",
|
||||
imagePrefix: "myorg",
|
||||
});
|
||||
const result = getRegistryTag(registry, "myorg/myprivaterepo");
|
||||
// Should not duplicate prefix
|
||||
expect(result).toBe("docker.io/myorg/myprivaterepo");
|
||||
});
|
||||
});
|
||||
|
||||
describe("without registryUrl", () => {
|
||||
it("should work without registryUrl", () => {
|
||||
const registry = createMockRegistry({
|
||||
username: "myuser",
|
||||
registryUrl: "",
|
||||
});
|
||||
const result = getRegistryTag(registry, "nginx");
|
||||
expect(result).toBe("myuser/nginx");
|
||||
});
|
||||
|
||||
it("should work without registryUrl with imagePrefix", () => {
|
||||
const registry = createMockRegistry({
|
||||
username: "myuser",
|
||||
imagePrefix: "myorg",
|
||||
registryUrl: "",
|
||||
});
|
||||
const result = getRegistryTag(registry, "nginx");
|
||||
expect(result).toBe("myorg/nginx");
|
||||
});
|
||||
|
||||
it("should handle username already present without registryUrl", () => {
|
||||
const registry = createMockRegistry({
|
||||
username: "myuser",
|
||||
registryUrl: "",
|
||||
});
|
||||
const result = getRegistryTag(registry, "myuser/myprivaterepo");
|
||||
// Should not duplicate username
|
||||
expect(result).toBe("myuser/myprivaterepo");
|
||||
});
|
||||
});
|
||||
|
||||
describe("with custom registryUrl", () => {
|
||||
it("should handle custom registry URL", () => {
|
||||
const registry = createMockRegistry({
|
||||
username: "myuser",
|
||||
registryUrl: "ghcr.io",
|
||||
});
|
||||
const result = getRegistryTag(registry, "nginx");
|
||||
expect(result).toBe("ghcr.io/myuser/nginx");
|
||||
});
|
||||
|
||||
it("should handle custom registry URL with imagePrefix", () => {
|
||||
const registry = createMockRegistry({
|
||||
username: "myuser",
|
||||
imagePrefix: "myorg",
|
||||
registryUrl: "ghcr.io",
|
||||
});
|
||||
const result = getRegistryTag(registry, "nginx");
|
||||
expect(result).toBe("ghcr.io/myorg/nginx");
|
||||
});
|
||||
|
||||
it("should handle custom registry URL with username already present", () => {
|
||||
const registry = createMockRegistry({
|
||||
username: "myuser",
|
||||
registryUrl: "ghcr.io",
|
||||
});
|
||||
const result = getRegistryTag(registry, "myuser/myprivaterepo");
|
||||
// Should not duplicate username
|
||||
expect(result).toBe("ghcr.io/myuser/myprivaterepo");
|
||||
});
|
||||
});
|
||||
|
||||
describe("edge cases", () => {
|
||||
it("should handle empty image name", () => {
|
||||
const registry = createMockRegistry({ username: "myuser" });
|
||||
const result = getRegistryTag(registry, "");
|
||||
expect(result).toBe("docker.io/myuser/");
|
||||
});
|
||||
|
||||
it("should handle image name with multiple slashes", () => {
|
||||
const registry = createMockRegistry({ username: "myuser" });
|
||||
const result = getRegistryTag(registry, "org/suborg/repo");
|
||||
expect(result).toBe("docker.io/myuser/repo");
|
||||
});
|
||||
|
||||
it("should handle image name with username at different position", () => {
|
||||
const registry = createMockRegistry({ username: "myuser" });
|
||||
const result = getRegistryTag(registry, "org/myuser/repo");
|
||||
expect(result).toBe("docker.io/myuser/repo");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -28,6 +28,7 @@ const baseApp: ApplicationNested = {
|
||||
railpackVersion: "0.2.2",
|
||||
applicationId: "",
|
||||
previewLabels: [],
|
||||
createEnvFile: true,
|
||||
herokuVersion: "",
|
||||
giteaBranch: "",
|
||||
buildServerId: "",
|
||||
@@ -67,6 +68,7 @@ const baseApp: ApplicationNested = {
|
||||
previewWildcard: "",
|
||||
environment: {
|
||||
env: "",
|
||||
isDefault: false,
|
||||
environmentId: "",
|
||||
name: "",
|
||||
createdAt: "",
|
||||
|
||||
@@ -54,4 +54,22 @@ describe("processLogs", () => {
|
||||
const result = parseRawConfig(entryWithWhitespace);
|
||||
expect(result.data).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("should filter out Dokploy dashboard requests", () => {
|
||||
const dokployDashboardEntry = `{"ClientAddr":"172.71.187.131:9485","ClientHost":"172.71.187.131","ClientPort":"9485","ClientUsername":"-","DownstreamContentSize":14550,"DownstreamStatus":200,"Duration":57681682,"OriginContentSize":14550,"OriginDuration":57612242,"OriginStatus":200,"Overhead":69440,"RequestAddr":"hostinger.dokploy.com","RequestContentSize":0,"RequestCount":20142,"RequestHost":"hostinger.dokploy.com","RequestMethod":"GET","RequestPath":"/_next/data/cb_zzI4Rp9G7Q7djrFKh0/en/dashboard/traefik.json","RequestPort":"-","RequestProtocol":"HTTP/2.0","RequestScheme":"https","RetryAttempts":0,"RouterName":"dokploy-router-app-secure@file","ServiceAddr":"dokploy:3000","ServiceName":"dokploy-service-app@file","ServiceURL":"http://dokploy:3000","StartLocal":"2025-12-10T05:10:41.957755949Z","StartUTC":"2025-12-10T05:10:41.957755949Z","TLSCipher":"TLS_AES_128_GCM_SHA256","TLSVersion":"1.3","entryPointName":"websecure","level":"info","msg":"","time":"2025-12-10T05:10:42Z"}`;
|
||||
|
||||
// Test with only Dokploy dashboard entry - should be filtered out
|
||||
const resultOnlyDokploy = parseRawConfig(dokployDashboardEntry);
|
||||
expect(resultOnlyDokploy.data).toHaveLength(0);
|
||||
expect(resultOnlyDokploy.totalCount).toBe(0);
|
||||
|
||||
// Test with mixed entries - Dokploy should be filtered, others should remain
|
||||
const mixedEntries = `${dokployDashboardEntry}\n${sampleLogEntry}`;
|
||||
const resultMixed = parseRawConfig(mixedEntries);
|
||||
expect(resultMixed.data).toHaveLength(1);
|
||||
expect(resultMixed.totalCount).toBe(1);
|
||||
expect(resultMixed.data[0]?.ServiceName).not.toBe(
|
||||
"dokploy-service-app@file",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ const baseApp: ApplicationNested = {
|
||||
rollbackActive: false,
|
||||
applicationId: "",
|
||||
previewLabels: [],
|
||||
createEnvFile: true,
|
||||
herokuVersion: "",
|
||||
giteaRepository: "",
|
||||
giteaOwner: "",
|
||||
@@ -49,6 +50,7 @@ const baseApp: ApplicationNested = {
|
||||
environmentId: "",
|
||||
environment: {
|
||||
env: "",
|
||||
isDefault: false,
|
||||
environmentId: "",
|
||||
name: "",
|
||||
createdAt: "",
|
||||
|
||||
@@ -208,6 +208,8 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||
const certificateType = form.watch("certificateType");
|
||||
const https = form.watch("https");
|
||||
const domainType = form.watch("domainType");
|
||||
const host = form.watch("host");
|
||||
const isTraefikMeDomain = host?.includes("traefik.me") || false;
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
@@ -502,6 +504,13 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||
to make your traefik.me domain work.
|
||||
</AlertBlock>
|
||||
)}
|
||||
{isTraefikMeDomain && (
|
||||
<AlertBlock type="info">
|
||||
<strong>Note:</strong> traefik.me is a public HTTP
|
||||
service and does not support SSL/HTTPS. HTTPS and
|
||||
certificate options will not have any effect.
|
||||
</AlertBlock>
|
||||
)}
|
||||
<FormLabel>Host</FormLabel>
|
||||
<div className="flex gap-2">
|
||||
<FormControl>
|
||||
|
||||
@@ -5,14 +5,23 @@ import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
} from "@/components/ui/form";
|
||||
import { Secrets } from "@/components/ui/secrets";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
const addEnvironmentSchema = z.object({
|
||||
env: z.string(),
|
||||
buildArgs: z.string(),
|
||||
buildSecrets: z.string(),
|
||||
createEnvFile: z.boolean(),
|
||||
});
|
||||
|
||||
type EnvironmentSchema = z.infer<typeof addEnvironmentSchema>;
|
||||
@@ -39,6 +48,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
env: "",
|
||||
buildArgs: "",
|
||||
buildSecrets: "",
|
||||
createEnvFile: true,
|
||||
},
|
||||
resolver: zodResolver(addEnvironmentSchema),
|
||||
});
|
||||
@@ -47,10 +57,12 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
const currentEnv = form.watch("env");
|
||||
const currentBuildArgs = form.watch("buildArgs");
|
||||
const currentBuildSecrets = form.watch("buildSecrets");
|
||||
const currentCreateEnvFile = form.watch("createEnvFile");
|
||||
const hasChanges =
|
||||
currentEnv !== (data?.env || "") ||
|
||||
currentBuildArgs !== (data?.buildArgs || "") ||
|
||||
currentBuildSecrets !== (data?.buildSecrets || "");
|
||||
currentBuildSecrets !== (data?.buildSecrets || "") ||
|
||||
currentCreateEnvFile !== (data?.createEnvFile ?? true);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
@@ -58,6 +70,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
env: data.env || "",
|
||||
buildArgs: data.buildArgs || "",
|
||||
buildSecrets: data.buildSecrets || "",
|
||||
createEnvFile: data.createEnvFile ?? true,
|
||||
});
|
||||
}
|
||||
}, [data, form]);
|
||||
@@ -67,6 +80,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
env: formData.env,
|
||||
buildArgs: formData.buildArgs,
|
||||
buildSecrets: formData.buildSecrets,
|
||||
createEnvFile: formData.createEnvFile,
|
||||
applicationId,
|
||||
})
|
||||
.then(async () => {
|
||||
@@ -83,6 +97,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
env: data?.env || "",
|
||||
buildArgs: data?.buildArgs || "",
|
||||
buildSecrets: data?.buildSecrets || "",
|
||||
createEnvFile: data?.createEnvFile ?? true,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -167,6 +182,30 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
placeholder="NPM_TOKEN=xyz"
|
||||
/>
|
||||
)}
|
||||
{data?.buildType === "dockerfile" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="createEnvFile"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 border rounded-lg shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Create Environment File</FormLabel>
|
||||
<FormDescription>
|
||||
When enabled, an .env file will be created during the
|
||||
build process. Disable this if you don't want to generate
|
||||
an environment file.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-row justify-end gap-2">
|
||||
{hasChanges && (
|
||||
<Button type="button" variant="outline" onClick={handleCancel}>
|
||||
|
||||
@@ -86,6 +86,9 @@ export const AddPreviewDomain = ({
|
||||
resolver: zodResolver(domain),
|
||||
});
|
||||
|
||||
const host = form.watch("host");
|
||||
const isTraefikMeDomain = host?.includes("traefik.me") || false;
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
@@ -157,6 +160,13 @@ export const AddPreviewDomain = ({
|
||||
name="host"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
{isTraefikMeDomain && (
|
||||
<AlertBlock type="info">
|
||||
<strong>Note:</strong> traefik.me is a public HTTP
|
||||
service and does not support SSL/HTTPS. HTTPS and
|
||||
certificate options will not have any effect.
|
||||
</AlertBlock>
|
||||
)}
|
||||
<FormLabel>Host</FormLabel>
|
||||
<div className="flex gap-2">
|
||||
<FormControl>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -100,6 +101,8 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
||||
});
|
||||
|
||||
const previewHttps = form.watch("previewHttps");
|
||||
const wildcardDomain = form.watch("wildcardDomain");
|
||||
const isTraefikMeDomain = wildcardDomain?.includes("traefik.me") || false;
|
||||
|
||||
useEffect(() => {
|
||||
setIsEnabled(data?.isPreviewDeploymentsActive || false);
|
||||
@@ -168,6 +171,13 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4">
|
||||
{isTraefikMeDomain && (
|
||||
<AlertBlock type="info">
|
||||
<strong>Note:</strong> traefik.me is a public HTTP service and
|
||||
does not support SSL/HTTPS. HTTPS and certificate options will
|
||||
not have any effect.
|
||||
</AlertBlock>
|
||||
)}
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
|
||||
@@ -102,7 +102,9 @@ export const AdvancedEnvironmentSelector = ({
|
||||
setName("");
|
||||
setDescription("");
|
||||
} catch (error) {
|
||||
toast.error("Failed to create environment");
|
||||
toast.error(
|
||||
`Failed to create environment: ${error instanceof Error ? error.message : error}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -123,7 +125,9 @@ export const AdvancedEnvironmentSelector = ({
|
||||
setName("");
|
||||
setDescription("");
|
||||
} catch (error) {
|
||||
toast.error("Failed to update environment");
|
||||
toast.error(
|
||||
`Failed to update environment: ${error instanceof Error ? error.message : error}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -140,15 +144,18 @@ export const AdvancedEnvironmentSelector = ({
|
||||
setIsDeleteDialogOpen(false);
|
||||
setSelectedEnvironment(null);
|
||||
|
||||
// Redirect to production if we deleted the current environment
|
||||
// Redirect to first available environment if we deleted the current environment
|
||||
if (selectedEnvironment.environmentId === currentEnvironmentId) {
|
||||
const productionEnv = environments?.find(
|
||||
(env) => env.name === "production",
|
||||
const firstEnv = environments?.find(
|
||||
(env) => env.environmentId !== selectedEnvironment.environmentId,
|
||||
);
|
||||
if (productionEnv) {
|
||||
if (firstEnv) {
|
||||
router.push(
|
||||
`/dashboard/project/${projectId}/environment/${productionEnv.environmentId}`,
|
||||
`/dashboard/project/${projectId}/environment/${firstEnv.environmentId}`,
|
||||
);
|
||||
} else {
|
||||
// No other environments, redirect to project page
|
||||
router.push(`/dashboard/project/${projectId}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -239,8 +246,8 @@ export const AdvancedEnvironmentSelector = ({
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
{environment.name !== "production" && (
|
||||
<div className="flex items-center gap-1 px-2">
|
||||
<div className="flex items-center gap-1 px-2">
|
||||
{!environment.isDefault && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@@ -252,22 +259,21 @@ export const AdvancedEnvironmentSelector = ({
|
||||
>
|
||||
<PencilIcon className="h-3 w-3" />
|
||||
</Button>
|
||||
|
||||
{canDeleteEnvironments && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0 text-red-600 hover:text-red-700"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openDeleteDialog(environment);
|
||||
}}
|
||||
>
|
||||
<TrashIcon className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
{canDeleteEnvironments && !environment.isDefault && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0 text-red-600 hover:text-red-700"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openDeleteDialog(environment);
|
||||
}}
|
||||
>
|
||||
<TrashIcon className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -89,24 +89,26 @@ export const SearchCommand = () => {
|
||||
<CommandGroup heading={"Projects"}>
|
||||
<CommandList>
|
||||
{data?.map((project) => {
|
||||
const productionEnvironment = project.environments.find(
|
||||
(environment) => environment.name === "production",
|
||||
);
|
||||
// Find default environment, or fall back to first environment
|
||||
const defaultEnvironment =
|
||||
project.environments.find(
|
||||
(environment) => environment.isDefault,
|
||||
) || project?.environments?.[0];
|
||||
|
||||
if (!productionEnvironment) return null;
|
||||
if (!defaultEnvironment) return null;
|
||||
|
||||
return (
|
||||
<CommandItem
|
||||
key={project.projectId}
|
||||
onSelect={() => {
|
||||
router.push(
|
||||
`/dashboard/project/${project.projectId}/environment/${productionEnvironment!.environmentId}`,
|
||||
`/dashboard/project/${project.projectId}/environment/${defaultEnvironment.environmentId}`,
|
||||
);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<BookIcon className="size-4 text-muted-foreground mr-2" />
|
||||
{project.name} / {productionEnvironment!.name}
|
||||
{project.name} / {defaultEnvironment.name}
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -45,7 +45,34 @@ const AddRegistrySchema = z.object({
|
||||
password: z.string().min(1, {
|
||||
message: "Password is required",
|
||||
}),
|
||||
registryUrl: z.string(),
|
||||
registryUrl: z
|
||||
.string()
|
||||
.optional()
|
||||
.refine(
|
||||
(val) => {
|
||||
// If empty or undefined, skip validation (field is optional)
|
||||
if (!val || val.trim().length === 0) {
|
||||
return true;
|
||||
}
|
||||
// Validate that it's a valid hostname (no protocol, no path, optional port)
|
||||
// Valid formats: example.com, registry.example.com, [::1], example.com:5000
|
||||
// Invalid: https://example.com, example.com/path
|
||||
const trimmed = val.trim();
|
||||
// Check for protocol or path - these are not allowed
|
||||
if (/^https?:\/\//i.test(trimmed) || trimmed.includes("/")) {
|
||||
return false;
|
||||
}
|
||||
// Basic hostname validation: allow alphanumeric, dots, hyphens, underscores, and IPv6 in brackets
|
||||
// Allow optional port at the end
|
||||
const hostnameRegex =
|
||||
/^(?:\[[^\]]+\]|[a-zA-Z0-9](?:[a-zA-Z0-9._-]{0,253}[a-zA-Z0-9])?)(?::\d+)?$/;
|
||||
return hostnameRegex.test(trimmed);
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Invalid registry URL. Please enter only the hostname (e.g., example.com or registry.example.com). Do not include protocol (https://) or paths.",
|
||||
},
|
||||
),
|
||||
imagePrefix: z.string(),
|
||||
serverId: z.string().optional(),
|
||||
});
|
||||
@@ -99,6 +126,9 @@ export const HandleRegistry = ({ registryId }: Props) => {
|
||||
const registryName = form.watch("registryName");
|
||||
const imagePrefix = form.watch("imagePrefix");
|
||||
const serverId = form.watch("serverId");
|
||||
const selectedServer = servers?.find(
|
||||
(server) => server.serverId === serverId,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (registry) {
|
||||
@@ -125,7 +155,7 @@ export const HandleRegistry = ({ registryId }: Props) => {
|
||||
password: data.password,
|
||||
registryName: data.registryName,
|
||||
username: data.username,
|
||||
registryUrl: data.registryUrl,
|
||||
registryUrl: data.registryUrl || "",
|
||||
registryType: "cloud",
|
||||
imagePrefix: data.imagePrefix,
|
||||
serverId: data.serverId,
|
||||
@@ -261,6 +291,10 @@ export const HandleRegistry = ({ registryId }: Props) => {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Registry URL</FormLabel>
|
||||
<FormDescription>
|
||||
Enter only the hostname (e.g.,
|
||||
aws_account_id.dkr.ecr.us-west-2.amazonaws.com).
|
||||
</FormDescription>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="aws_account_id.dkr.ecr.us-west-2.amazonaws.com"
|
||||
@@ -282,8 +316,40 @@ export const HandleRegistry = ({ registryId }: Props) => {
|
||||
<FormItem>
|
||||
<FormLabel>Server {!isCloud && "(Optional)"}</FormLabel>
|
||||
<FormDescription>
|
||||
Select a server to test the registry. this will run the
|
||||
following command on the server
|
||||
{!isCloud ? (
|
||||
<>
|
||||
{serverId && serverId !== "none" && selectedServer ? (
|
||||
<>
|
||||
Authentication will be performed on{" "}
|
||||
<strong>{selectedServer.name}</strong>. This
|
||||
registry will be available on this server.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Choose where to authenticate with the registry. By
|
||||
default, authentication occurs on the Dokploy
|
||||
server. Select a specific server to authenticate
|
||||
from that server instead.
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{serverId && serverId !== "none" && selectedServer ? (
|
||||
<>
|
||||
Authentication will be performed on{" "}
|
||||
<strong>{selectedServer.name}</strong>. This
|
||||
registry will be available on this server.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Select a server to authenticate with the registry.
|
||||
The authentication will be performed from the
|
||||
selected server.
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormControl>
|
||||
<Select
|
||||
@@ -345,7 +411,7 @@ export const HandleRegistry = ({ registryId }: Props) => {
|
||||
await testRegistry({
|
||||
username: username,
|
||||
password: password,
|
||||
registryUrl: registryUrl,
|
||||
registryUrl: registryUrl || "",
|
||||
registryName: registryName,
|
||||
registryType: "cloud",
|
||||
imagePrefix: imagePrefix,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { api } from "@/utils/api";
|
||||
import { ImpersonationBar } from "../dashboard/impersonation/impersonation-bar";
|
||||
import { ChatwootWidget } from "../shared/ChatwootWidget";
|
||||
import { HubSpotWidget } from "../shared/HubSpotWidget";
|
||||
import Page from "./side";
|
||||
|
||||
interface Props {
|
||||
@@ -25,7 +25,9 @@ export const DashboardLayout = ({ children }: Props) => {
|
||||
<>
|
||||
<Page>{children}</Page>
|
||||
{isCloud === true && isUserSubscribed === true && (
|
||||
<ChatwootWidget websiteToken="USCpQRKzHvFMssf3p6Eacae5" />
|
||||
<>
|
||||
<HubSpotWidget />
|
||||
</>
|
||||
)}
|
||||
|
||||
{haveRootAccess === true && <ImpersonationBar />}
|
||||
|
||||
14
apps/dokploy/components/shared/HubSpotWidget.tsx
Normal file
14
apps/dokploy/components/shared/HubSpotWidget.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import Script from "next/script";
|
||||
|
||||
export const HubSpotWidget = () => {
|
||||
return (
|
||||
<Script
|
||||
id="hs-script-loader"
|
||||
type="text/javascript"
|
||||
src="//js-eu1.hs-scripts.com/147033433.js"
|
||||
strategy="lazyOnload"
|
||||
async
|
||||
defer
|
||||
/>
|
||||
);
|
||||
};
|
||||
1
apps/dokploy/drizzle/0131_volatile_beast.sql
Normal file
1
apps/dokploy/drizzle/0131_volatile_beast.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "application" ADD COLUMN "createEnvFile" boolean DEFAULT true NOT NULL;
|
||||
4
apps/dokploy/drizzle/0132_clean_layla_miller.sql
Normal file
4
apps/dokploy/drizzle/0132_clean_layla_miller.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE "environment" ADD COLUMN "isDefault" boolean DEFAULT false NOT NULL;
|
||||
|
||||
-- Set isDefault to true for existing production environments
|
||||
UPDATE "environment" SET "isDefault" = true WHERE "name" = 'production';
|
||||
6928
apps/dokploy/drizzle/meta/0131_snapshot.json
Normal file
6928
apps/dokploy/drizzle/meta/0131_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
6935
apps/dokploy/drizzle/meta/0132_snapshot.json
Normal file
6935
apps/dokploy/drizzle/meta/0132_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -918,6 +918,20 @@
|
||||
"when": 1765167657813,
|
||||
"tag": "0130_perpetual_screwball",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 131,
|
||||
"version": "7",
|
||||
"when": 1765342621312,
|
||||
"tag": "0131_volatile_beast",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 132,
|
||||
"version": "7",
|
||||
"when": 1765346573500,
|
||||
"tag": "0132_clean_layla_miller",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dokploy",
|
||||
"version": "v0.26.0",
|
||||
"version": "v0.26.1",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -81,41 +81,34 @@ export default async function handler(
|
||||
return;
|
||||
}
|
||||
|
||||
if (!webhookImageName) {
|
||||
res.status(301).json({
|
||||
message: "Webhook Docker Image Name Not Found",
|
||||
});
|
||||
return;
|
||||
}
|
||||
// If webhook provides image information, validate it matches the configured image
|
||||
// If webhook doesn't provide image information, fall back to using the configured image (backward compatibility)
|
||||
if (webhookImageName) {
|
||||
// Validate image name matches
|
||||
if (webhookImageName !== applicationImageName) {
|
||||
res.status(301).json({
|
||||
message: `Application Image Name (${applicationImageName}) doesn't match request event payload Image Name (${webhookImageName}).`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate image name matches
|
||||
if (webhookImageName !== applicationImageName) {
|
||||
res.status(301).json({
|
||||
message: `Application Image Name (${applicationImageName}) doesn't match request event payload Image Name (${webhookImageName}).`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!applicationDockerTag) {
|
||||
res.status(301).json({
|
||||
message: "Application Docker Tag Not Found",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!applicationDockerTag) {
|
||||
res.status(301).json({
|
||||
message: "Application Docker Tag Not Found",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!webhookDockerTag) {
|
||||
res.status(301).json({
|
||||
message: "Webhook Docker Tag Not Found",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (webhookDockerTag !== applicationDockerTag) {
|
||||
res.status(301).json({
|
||||
message: `Application Image Tag (${applicationDockerTag}) doesn't match request event payload Image Tag (${webhookDockerTag}).`,
|
||||
});
|
||||
return;
|
||||
if (webhookDockerTag) {
|
||||
if (webhookDockerTag !== applicationDockerTag) {
|
||||
res.status(301).json({
|
||||
message: `Application Image Tag (${applicationDockerTag}) doesn't match request event payload Image Tag (${webhookDockerTag}).`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// If webhook doesn't provide image info, we'll use the configured image (old behavior)
|
||||
} else if (sourceType === "github") {
|
||||
const normalizedCommits = req.body?.commits?.flatMap(
|
||||
(commit: any) => commit.modified,
|
||||
|
||||
@@ -365,6 +365,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
env: input.env,
|
||||
buildArgs: input.buildArgs,
|
||||
buildSecrets: input.buildSecrets,
|
||||
createEnvFile: input.createEnvFile,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
|
||||
@@ -66,10 +66,12 @@ export const environmentRouter = createTRPCRouter({
|
||||
if (input.name === "production") {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Environment name cannot be production",
|
||||
message:
|
||||
"You cannot create a environment with the name 'production'",
|
||||
});
|
||||
}
|
||||
|
||||
// Allow users to create environments with any name, including "production"
|
||||
const environment = await createEnvironment(input);
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
@@ -243,13 +245,7 @@ export const environmentRouter = createTRPCRouter({
|
||||
try {
|
||||
const { environmentId, ...updateData } = input;
|
||||
|
||||
if (updateData.name === "production") {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Environment name cannot be production",
|
||||
});
|
||||
}
|
||||
|
||||
// Allow users to rename environments to any name, including "production"
|
||||
if (ctx.user.role === "member") {
|
||||
await checkEnvironmentAccess(
|
||||
ctx.user.id,
|
||||
@@ -259,6 +255,13 @@ export const environmentRouter = createTRPCRouter({
|
||||
);
|
||||
}
|
||||
const currentEnvironment = await findEnvironmentById(environmentId);
|
||||
|
||||
if (currentEnvironment.isDefault) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "You cannot update the default environment",
|
||||
});
|
||||
}
|
||||
if (
|
||||
currentEnvironment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
|
||||
@@ -247,3 +247,18 @@
|
||||
.cm-lineWrapping {
|
||||
@apply font-mono;
|
||||
}
|
||||
|
||||
/* HubSpot Widget - Force light color-scheme to prevent white background */
|
||||
#hubspot-messages-iframe-container,
|
||||
#hubspot-messages-iframe-container * {
|
||||
background-color: transparent !important;
|
||||
color-scheme: light !important;
|
||||
}
|
||||
|
||||
#hubspot-messages-iframe-container .hs-shadow-container {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#hubspot-conversations-iframe {
|
||||
color-scheme: light !important;
|
||||
}
|
||||
|
||||
@@ -181,6 +181,7 @@ export const applications = pgTable("application", {
|
||||
herokuVersion: text("herokuVersion").default("24"),
|
||||
publishDirectory: text("publishDirectory"),
|
||||
isStaticSpa: boolean("isStaticSpa"),
|
||||
createEnvFile: boolean("createEnvFile").notNull().default(true),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
@@ -332,6 +333,7 @@ const createSchema = createInsertSchema(applications, {
|
||||
herokuVersion: z.string().optional(),
|
||||
publishDirectory: z.string().optional(),
|
||||
isStaticSpa: z.boolean().optional(),
|
||||
createEnvFile: z.boolean().optional(),
|
||||
owner: z.string(),
|
||||
healthCheckSwarm: HealthCheckSwarmSchema.nullable(),
|
||||
restartPolicySwarm: RestartPolicySwarmSchema.nullable(),
|
||||
@@ -501,6 +503,7 @@ export const apiSaveEnvironmentVariables = createSchema
|
||||
env: true,
|
||||
buildArgs: true,
|
||||
buildSecrets: true,
|
||||
createEnvFile: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { boolean, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
@@ -26,6 +26,7 @@ export const environments = pgTable("environment", {
|
||||
projectId: text("projectId")
|
||||
.notNull()
|
||||
.references(() => projects.projectId, { onDelete: "cascade" }),
|
||||
isDefault: boolean("isDefault").notNull().default(false),
|
||||
});
|
||||
|
||||
export const environmentRelations = relations(
|
||||
@@ -69,9 +70,14 @@ export const apiRemoveEnvironment = createSchema
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiUpdateEnvironment = createSchema.partial().extend({
|
||||
environmentId: z.string().min(1),
|
||||
});
|
||||
export const apiUpdateEnvironment = createSchema
|
||||
.partial()
|
||||
.extend({
|
||||
environmentId: z.string().min(1),
|
||||
})
|
||||
.omit({
|
||||
isDefault: true,
|
||||
});
|
||||
|
||||
export const apiDuplicateEnvironment = createSchema
|
||||
.pick({
|
||||
|
||||
@@ -103,10 +103,10 @@ export const findEnvironmentsByProjectId = async (projectId: string) => {
|
||||
|
||||
export const deleteEnvironment = async (environmentId: string) => {
|
||||
const currentEnvironment = await findEnvironmentById(environmentId);
|
||||
if (currentEnvironment.name === "production") {
|
||||
if (currentEnvironment.isDefault) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "You cannot delete the production environment",
|
||||
message: "You cannot delete the default environment",
|
||||
});
|
||||
}
|
||||
const deletedEnvironment = await db
|
||||
@@ -162,9 +162,23 @@ export const duplicateEnvironment = async (
|
||||
};
|
||||
|
||||
export const createProductionEnvironment = async (projectId: string) => {
|
||||
return createEnvironment({
|
||||
name: "production",
|
||||
description: "Production environment",
|
||||
projectId,
|
||||
});
|
||||
const newEnvironment = await db
|
||||
.insert(environments)
|
||||
.values({
|
||||
name: "production",
|
||||
description: "Production environment",
|
||||
projectId,
|
||||
isDefault: true,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newEnvironment) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error creating the production environment",
|
||||
});
|
||||
}
|
||||
|
||||
return newEnvironment;
|
||||
};
|
||||
|
||||
@@ -99,6 +99,11 @@ export function parseRawConfig(
|
||||
.compact()
|
||||
.value();
|
||||
|
||||
// Filter out Dokploy dashboard requests
|
||||
parsedLogs = parsedLogs.filter(
|
||||
(log) => log.ServiceName !== "dokploy-service-app@file",
|
||||
);
|
||||
|
||||
// Apply date range filter if provided
|
||||
if (dateRange?.start || dateRange?.end) {
|
||||
parsedLogs = parsedLogs.filter((log) => {
|
||||
|
||||
@@ -25,7 +25,7 @@ export const initCronJobs = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (admin.user.enableDockerCleanup) {
|
||||
if (admin?.user?.enableDockerCleanup) {
|
||||
scheduleJob("docker-cleanup", "0 0 * * *", async () => {
|
||||
console.log(
|
||||
`Docker Cleanup ${new Date().toLocaleString()}] Running docker cleanup`,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
getDockerContextPath,
|
||||
} from "../filesystem/directory";
|
||||
import type { ApplicationNested } from ".";
|
||||
import { createEnvFileCommand } from "./utils";
|
||||
|
||||
export const getDockerCommand = (application: ApplicationNested) => {
|
||||
const {
|
||||
@@ -18,6 +19,7 @@ export const getDockerCommand = (application: ApplicationNested) => {
|
||||
buildSecrets,
|
||||
dockerBuildStage,
|
||||
cleanCache,
|
||||
createEnvFile,
|
||||
} = application;
|
||||
const dockerFilePath = getBuildAppDirectory(application);
|
||||
|
||||
@@ -60,6 +62,21 @@ export const getDockerCommand = (application: ApplicationNested) => {
|
||||
.map(([key, value]) => `${key}=${quote([value])}`)
|
||||
.join(" ");
|
||||
|
||||
/*
|
||||
Do not generate an environment file when publishDirectory is specified,
|
||||
as it could be publicly exposed.
|
||||
Also respect the createEnvFile flag.
|
||||
*/
|
||||
let command = "";
|
||||
if (!publishDirectory && createEnvFile) {
|
||||
command += createEnvFileCommand(
|
||||
dockerFilePath,
|
||||
env,
|
||||
application.environment.project.env,
|
||||
application.environment.env,
|
||||
);
|
||||
}
|
||||
|
||||
for (const key in secrets) {
|
||||
// Although buildx is smart enough to know we may be referring to an environment variable name,
|
||||
// we still make sure it doesn't fall back to `type=file`.
|
||||
@@ -67,7 +84,7 @@ export const getDockerCommand = (application: ApplicationNested) => {
|
||||
commandArgs.push("--secret", `type=env,id=${key}`);
|
||||
}
|
||||
|
||||
const command = `
|
||||
command += `
|
||||
echo "Building ${appName}" ;
|
||||
cd ${dockerContextPath} || {
|
||||
echo "❌ The path ${dockerContextPath} does not exist" ;
|
||||
|
||||
@@ -74,11 +74,40 @@ export const uploadImageRemoteCommand = async (
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Extract the repository name from imageName by taking the last part after '/'
|
||||
* Examples:
|
||||
* - "nginx" -> "nginx"
|
||||
* - "nginx:latest" -> "nginx:latest"
|
||||
* - "myuser/myrepo" -> "myrepo"
|
||||
* - "myuser/myrepo:tag" -> "myrepo:tag"
|
||||
* - "docker.io/myuser/myrepo" -> "myrepo"
|
||||
*/
|
||||
const extractRepositoryName = (imageName: string): string => {
|
||||
const lastSlashIndex = imageName.lastIndexOf("/");
|
||||
|
||||
// If no '/', return the imageName as is
|
||||
if (lastSlashIndex === -1) {
|
||||
return imageName;
|
||||
}
|
||||
|
||||
// Extract everything after the last '/'
|
||||
return imageName.substring(lastSlashIndex + 1);
|
||||
};
|
||||
|
||||
export const getRegistryTag = (registry: Registry, imageName: string) => {
|
||||
const { registryUrl, imagePrefix, username } = registry;
|
||||
return imagePrefix
|
||||
? `${registryUrl ? `${registryUrl}/` : ""}${imagePrefix}/${imageName}`
|
||||
: `${registryUrl ? `${registryUrl}/` : ""}${username}/${imageName}`;
|
||||
|
||||
// Extract the repository name (last part after '/')
|
||||
const repositoryName = extractRepositoryName(imageName);
|
||||
|
||||
// Build the final tag using registry's username/prefix
|
||||
const targetPrefix = imagePrefix || username;
|
||||
const finalRegistry = registryUrl || "";
|
||||
|
||||
return finalRegistry
|
||||
? `${finalRegistry}/${targetPrefix}/${repositoryName}`
|
||||
: `${targetPrefix}/${repositoryName}`;
|
||||
};
|
||||
|
||||
const getRegistryCommands = (
|
||||
|
||||
@@ -162,6 +162,7 @@ export const readMonitoringConfig = async (readAll = false) => {
|
||||
trimmed.endsWith("}")
|
||||
) {
|
||||
const log = JSON.parse(trimmed);
|
||||
// Exclude Dokploy service app and Dashboard requests
|
||||
if (log.ServiceName !== "dokploy-service-app@file") {
|
||||
content += `${line}\n`;
|
||||
validCount++;
|
||||
|
||||
Reference in New Issue
Block a user