mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-16 12:45:21 +02:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17da1d5b3c | ||
|
|
f7613d9375 | ||
|
|
a43ad106f2 | ||
|
|
0e26c5023b | ||
|
|
f4a4530481 | ||
|
|
00dc3fae11 | ||
|
|
1da23f8888 | ||
|
|
bee4e4639c | ||
|
|
bd5b27ad51 | ||
|
|
b391abfd5c | ||
|
|
21a6657e00 | ||
|
|
d348ad5556 | ||
|
|
5d8b7b9b99 | ||
|
|
f5fa39b97e | ||
|
|
0a3a90c4e9 | ||
|
|
f440df343a | ||
|
|
4ec282b2f3 | ||
|
|
c039e638a6 | ||
|
|
65ffc63da4 | ||
|
|
5ba120567f | ||
|
|
8a335789b3 | ||
|
|
de2579401c | ||
|
|
6c90075a64 | ||
|
|
0a401843f8 |
@@ -147,6 +147,7 @@ const baseApp: ApplicationNested = {
|
||||
dockerContextPath: null,
|
||||
rollbackActive: false,
|
||||
stopGracePeriodSwarm: null,
|
||||
ulimitsSwarm: null,
|
||||
};
|
||||
|
||||
describe("unzipDrop using real zip files", () => {
|
||||
|
||||
@@ -6,6 +6,7 @@ type MockCreateServiceOptions = {
|
||||
TaskTemplate?: {
|
||||
ContainerSpec?: {
|
||||
StopGracePeriod?: number;
|
||||
Ulimits?: Array<{ Name: string; Soft: number; Hard: number }>;
|
||||
};
|
||||
};
|
||||
[key: string]: unknown;
|
||||
@@ -57,6 +58,7 @@ const createApplication = (
|
||||
},
|
||||
replicas: 1,
|
||||
stopGracePeriodSwarm: 0n,
|
||||
ulimitsSwarm: null,
|
||||
serverId: "server-id",
|
||||
...overrides,
|
||||
}) as unknown as ApplicationNested;
|
||||
@@ -110,4 +112,50 @@ describe("mechanizeDockerContainer", () => {
|
||||
"StopGracePeriod",
|
||||
);
|
||||
});
|
||||
|
||||
it("passes ulimits to ContainerSpec when ulimitsSwarm is defined", async () => {
|
||||
const ulimits = [
|
||||
{ Name: "nofile", Soft: 10000, Hard: 20000 },
|
||||
{ Name: "nproc", Soft: 4096, Hard: 8192 },
|
||||
];
|
||||
const application = createApplication({ ulimitsSwarm: ulimits });
|
||||
|
||||
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.TaskTemplate?.ContainerSpec?.Ulimits).toEqual(ulimits);
|
||||
});
|
||||
|
||||
it("omits Ulimits when ulimitsSwarm is null", async () => {
|
||||
const application = createApplication({ ulimitsSwarm: 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.TaskTemplate?.ContainerSpec).not.toHaveProperty("Ulimits");
|
||||
});
|
||||
|
||||
it("omits Ulimits when ulimitsSwarm is an empty array", async () => {
|
||||
const application = createApplication({ ulimitsSwarm: [] });
|
||||
|
||||
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.TaskTemplate?.ContainerSpec).not.toHaveProperty("Ulimits");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -125,6 +125,7 @@ const baseApp: ApplicationNested = {
|
||||
username: null,
|
||||
dockerContextPath: null,
|
||||
stopGracePeriodSwarm: null,
|
||||
ulimitsSwarm: null,
|
||||
};
|
||||
|
||||
const baseDomain: Domain = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import { InfoIcon, Plus, Trash2 } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useFieldArray, useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
@@ -21,10 +21,18 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
createConverter,
|
||||
NumberInputWithSteps,
|
||||
} from "@/components/ui/number-input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -50,13 +58,36 @@ const memoryConverter = createConverter(1024 * 1024, (mb) => {
|
||||
: `${formatNumber(mb)} MB`;
|
||||
});
|
||||
|
||||
const ulimitSchema = z.object({
|
||||
Name: z.string().min(1, "Name is required"),
|
||||
Soft: z.coerce.number().int().min(-1, "Must be >= -1"),
|
||||
Hard: z.coerce.number().int().min(-1, "Must be >= -1"),
|
||||
});
|
||||
|
||||
const addResourcesSchema = z.object({
|
||||
memoryReservation: z.string().optional(),
|
||||
cpuLimit: z.string().optional(),
|
||||
memoryLimit: z.string().optional(),
|
||||
cpuReservation: z.string().optional(),
|
||||
ulimitsSwarm: z.array(ulimitSchema).optional(),
|
||||
});
|
||||
|
||||
const ULIMIT_PRESETS = [
|
||||
{ value: "nofile", label: "nofile (Open Files)" },
|
||||
{ value: "nproc", label: "nproc (Processes)" },
|
||||
{ value: "memlock", label: "memlock (Locked Memory)" },
|
||||
{ value: "stack", label: "stack (Stack Size)" },
|
||||
{ value: "core", label: "core (Core File Size)" },
|
||||
{ value: "cpu", label: "cpu (CPU Time)" },
|
||||
{ value: "data", label: "data (Data Segment)" },
|
||||
{ value: "fsize", label: "fsize (File Size)" },
|
||||
{ value: "locks", label: "locks (File Locks)" },
|
||||
{ value: "msgqueue", label: "msgqueue (Message Queues)" },
|
||||
{ value: "nice", label: "nice (Nice Priority)" },
|
||||
{ value: "rtprio", label: "rtprio (Real-time Priority)" },
|
||||
{ value: "sigpending", label: "sigpending (Pending Signals)" },
|
||||
];
|
||||
|
||||
export type ServiceType =
|
||||
| "postgres"
|
||||
| "mongo"
|
||||
@@ -107,10 +138,16 @@ export const ShowResources = ({ id, type }: Props) => {
|
||||
cpuReservation: "",
|
||||
memoryLimit: "",
|
||||
memoryReservation: "",
|
||||
ulimitsSwarm: [],
|
||||
},
|
||||
resolver: zodResolver(addResourcesSchema),
|
||||
});
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control: form.control,
|
||||
name: "ulimitsSwarm",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
@@ -118,6 +155,7 @@ export const ShowResources = ({ id, type }: Props) => {
|
||||
cpuReservation: data?.cpuReservation || undefined,
|
||||
memoryLimit: data?.memoryLimit || undefined,
|
||||
memoryReservation: data?.memoryReservation || undefined,
|
||||
ulimitsSwarm: data?.ulimitsSwarm || [],
|
||||
});
|
||||
}
|
||||
}, [data, form, form.reset]);
|
||||
@@ -134,6 +172,10 @@ export const ShowResources = ({ id, type }: Props) => {
|
||||
cpuReservation: formData.cpuReservation || null,
|
||||
memoryLimit: formData.memoryLimit || null,
|
||||
memoryReservation: formData.memoryReservation || null,
|
||||
ulimitsSwarm:
|
||||
formData.ulimitsSwarm && formData.ulimitsSwarm.length > 0
|
||||
? formData.ulimitsSwarm
|
||||
: null,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Resources Updated");
|
||||
@@ -325,6 +367,145 @@ export const ShowResources = ({ id, type }: Props) => {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Ulimits Section */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<FormLabel className="text-base">Ulimits</FormLabel>
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-xs">
|
||||
<p>
|
||||
Set resource limits for the container. Each ulimit has
|
||||
a soft limit (warning threshold) and hard limit
|
||||
(maximum allowed). Use -1 for unlimited.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
append({ Name: "nofile", Soft: 65535, Hard: 65535 })
|
||||
}
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
Add Ulimit
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{fields.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
{fields.map((field, index) => (
|
||||
<div
|
||||
key={field.id}
|
||||
className="flex items-start gap-3 p-3 border rounded-lg bg-muted/30"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`ulimitsSwarm.${index}.Name`}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel className="text-xs">Type</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select ulimit" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{ULIMIT_PRESETS.map((preset) => (
|
||||
<SelectItem
|
||||
key={preset.value}
|
||||
value={preset.value}
|
||||
>
|
||||
{preset.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`ulimitsSwarm.${index}.Soft`}
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-32">
|
||||
<FormLabel className="text-xs">
|
||||
Soft Limit
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
min={-1}
|
||||
placeholder="65535"
|
||||
{...field}
|
||||
onChange={(e) =>
|
||||
field.onChange(Number(e.target.value))
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`ulimitsSwarm.${index}.Hard`}
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-32">
|
||||
<FormLabel className="text-xs">
|
||||
Hard Limit
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
min={-1}
|
||||
placeholder="65535"
|
||||
{...field}
|
||||
onChange={(e) =>
|
||||
field.onChange(Number(e.target.value))
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="mt-6 text-destructive hover:text-destructive"
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{fields.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No ulimits configured. Click "Add Ulimit" to set
|
||||
resource limits.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex w-full justify-end">
|
||||
<Button isLoading={isLoading} type="submit">
|
||||
Save
|
||||
|
||||
@@ -245,13 +245,13 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{isLoadingRepositories
|
||||
? "Loading...."
|
||||
: field.value.owner
|
||||
? repositories?.find(
|
||||
{!field.value.owner
|
||||
? "Select repository"
|
||||
: isLoadingRepositories
|
||||
? "Loading...."
|
||||
: (repositories?.find(
|
||||
(repo) => repo.name === field.value.repo,
|
||||
)?.name
|
||||
: "Select repository"}
|
||||
)?.name ?? "Select repository")}
|
||||
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
@@ -263,11 +263,15 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
||||
placeholder="Search repository..."
|
||||
className="h-9"
|
||||
/>
|
||||
{isLoadingRepositories && (
|
||||
{!bitbucketId ? (
|
||||
<span className="py-6 text-center text-sm text-muted-foreground">
|
||||
Select a Bitbucket account first
|
||||
</span>
|
||||
) : isLoadingRepositories ? (
|
||||
<span className="py-6 text-center text-sm">
|
||||
Loading Repositories....
|
||||
</span>
|
||||
)}
|
||||
) : null}
|
||||
<CommandEmpty>No repositories found.</CommandEmpty>
|
||||
<ScrollArea className="h-96">
|
||||
<CommandGroup>
|
||||
|
||||
@@ -258,14 +258,14 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{isLoadingRepositories
|
||||
? "Loading...."
|
||||
: field.value.owner
|
||||
? repositories?.find(
|
||||
{!field.value.owner
|
||||
? "Select repository"
|
||||
: isLoadingRepositories
|
||||
? "Loading...."
|
||||
: (repositories?.find(
|
||||
(repo: GiteaRepository) =>
|
||||
repo.name === field.value.repo,
|
||||
)?.name
|
||||
: "Select repository"}
|
||||
)?.name ?? "Select repository")}
|
||||
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
@@ -277,11 +277,15 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
||||
placeholder="Search repository..."
|
||||
className="h-9"
|
||||
/>
|
||||
{isLoadingRepositories && (
|
||||
{!giteaId ? (
|
||||
<span className="py-6 text-center text-sm text-muted-foreground">
|
||||
Select a Gitea account first
|
||||
</span>
|
||||
) : isLoadingRepositories ? (
|
||||
<span className="py-6 text-center text-sm">
|
||||
Loading Repositories....
|
||||
</span>
|
||||
)}
|
||||
) : null}
|
||||
<CommandEmpty>No repositories found.</CommandEmpty>
|
||||
<ScrollArea className="h-96">
|
||||
<CommandGroup>
|
||||
|
||||
@@ -233,13 +233,13 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{isLoadingRepositories
|
||||
? "Loading...."
|
||||
: field.value.owner
|
||||
? repositories?.find(
|
||||
{!field.value.owner
|
||||
? "Select repository"
|
||||
: isLoadingRepositories
|
||||
? "Loading...."
|
||||
: (repositories?.find(
|
||||
(repo) => repo.name === field.value.repo,
|
||||
)?.name
|
||||
: "Select repository"}
|
||||
)?.name ?? "Select repository")}
|
||||
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
@@ -251,11 +251,15 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
||||
placeholder="Search repository..."
|
||||
className="h-9"
|
||||
/>
|
||||
{isLoadingRepositories && (
|
||||
{!githubId ? (
|
||||
<span className="py-6 text-center text-sm text-muted-foreground">
|
||||
Select a GitHub account first
|
||||
</span>
|
||||
) : isLoadingRepositories ? (
|
||||
<span className="py-6 text-center text-sm">
|
||||
Loading Repositories....
|
||||
</span>
|
||||
)}
|
||||
) : null}
|
||||
<CommandEmpty>No repositories found.</CommandEmpty>
|
||||
<ScrollArea className="h-96">
|
||||
<CommandGroup>
|
||||
|
||||
@@ -254,13 +254,13 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{isLoadingRepositories
|
||||
? "Loading...."
|
||||
: field.value.owner
|
||||
? repositories?.find(
|
||||
{!field.value.owner
|
||||
? "Select repository"
|
||||
: isLoadingRepositories
|
||||
? "Loading...."
|
||||
: (repositories?.find(
|
||||
(repo) => repo.name === field.value.repo,
|
||||
)?.name
|
||||
: "Select repository"}
|
||||
)?.name ?? "Select repository")}
|
||||
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
@@ -272,11 +272,15 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
||||
placeholder="Search repository..."
|
||||
className="h-9"
|
||||
/>
|
||||
{isLoadingRepositories && (
|
||||
{!gitlabId ? (
|
||||
<span className="py-6 text-center text-sm text-muted-foreground">
|
||||
Select a GitLab account first
|
||||
</span>
|
||||
) : isLoadingRepositories ? (
|
||||
<span className="py-6 text-center text-sm">
|
||||
Loading Repositories....
|
||||
</span>
|
||||
)}
|
||||
) : null}
|
||||
<CommandEmpty>No repositories found.</CommandEmpty>
|
||||
<ScrollArea className="h-96">
|
||||
<CommandGroup>
|
||||
|
||||
@@ -247,13 +247,13 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{isLoadingRepositories
|
||||
? "Loading...."
|
||||
: field.value.owner
|
||||
? repositories?.find(
|
||||
{!field.value.owner
|
||||
? "Select repository"
|
||||
: isLoadingRepositories
|
||||
? "Loading...."
|
||||
: (repositories?.find(
|
||||
(repo) => repo.name === field.value.repo,
|
||||
)?.name
|
||||
: "Select repository"}
|
||||
)?.name ?? "Select repository")}
|
||||
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
@@ -265,11 +265,15 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
||||
placeholder="Search repository..."
|
||||
className="h-9"
|
||||
/>
|
||||
{isLoadingRepositories && (
|
||||
{!bitbucketId ? (
|
||||
<span className="py-6 text-center text-sm text-muted-foreground">
|
||||
Select a Bitbucket account first
|
||||
</span>
|
||||
) : isLoadingRepositories ? (
|
||||
<span className="py-6 text-center text-sm">
|
||||
Loading Repositories....
|
||||
</span>
|
||||
)}
|
||||
) : null}
|
||||
<CommandEmpty>No repositories found.</CommandEmpty>
|
||||
<ScrollArea className="h-96">
|
||||
<CommandGroup>
|
||||
|
||||
@@ -244,13 +244,13 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{isLoadingRepositories
|
||||
? "Loading...."
|
||||
: field.value.owner
|
||||
? repositories?.find(
|
||||
{!field.value.owner
|
||||
? "Select repository"
|
||||
: isLoadingRepositories
|
||||
? "Loading...."
|
||||
: (repositories?.find(
|
||||
(repo) => repo.name === field.value.repo,
|
||||
)?.name
|
||||
: "Select repository"}
|
||||
)?.name ?? "Select repository")}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
@@ -261,11 +261,15 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
|
||||
placeholder="Search repository..."
|
||||
className="h-9"
|
||||
/>
|
||||
{isLoadingRepositories && (
|
||||
{!giteaId ? (
|
||||
<span className="py-6 text-center text-sm text-muted-foreground">
|
||||
Select a Gitea account first
|
||||
</span>
|
||||
) : isLoadingRepositories ? (
|
||||
<span className="py-6 text-center text-sm">
|
||||
Loading Repositories....
|
||||
</span>
|
||||
)}
|
||||
) : null}
|
||||
<CommandEmpty>No repositories found.</CommandEmpty>
|
||||
<ScrollArea className="h-96">
|
||||
<CommandGroup>
|
||||
|
||||
@@ -234,13 +234,13 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{isLoadingRepositories
|
||||
? "Loading...."
|
||||
: field.value.owner
|
||||
? repositories?.find(
|
||||
{!field.value.owner
|
||||
? "Select repository"
|
||||
: isLoadingRepositories
|
||||
? "Loading...."
|
||||
: (repositories?.find(
|
||||
(repo) => repo.name === field.value.repo,
|
||||
)?.name
|
||||
: "Select repository"}
|
||||
)?.name ?? "Select repository")}
|
||||
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
@@ -252,11 +252,15 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
||||
placeholder="Search repository..."
|
||||
className="h-9"
|
||||
/>
|
||||
{isLoadingRepositories && (
|
||||
{!githubId ? (
|
||||
<span className="py-6 text-center text-sm text-muted-foreground">
|
||||
Select a GitHub account first
|
||||
</span>
|
||||
) : isLoadingRepositories ? (
|
||||
<span className="py-6 text-center text-sm">
|
||||
Loading Repositories....
|
||||
</span>
|
||||
)}
|
||||
) : null}
|
||||
<CommandEmpty>No repositories found.</CommandEmpty>
|
||||
<ScrollArea className="h-96">
|
||||
<CommandGroup>
|
||||
|
||||
@@ -256,13 +256,13 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{isLoadingRepositories
|
||||
? "Loading...."
|
||||
: field.value.owner
|
||||
? repositories?.find(
|
||||
{!field.value.owner
|
||||
? "Select repository"
|
||||
: isLoadingRepositories
|
||||
? "Loading...."
|
||||
: (repositories?.find(
|
||||
(repo) => repo.name === field.value.repo,
|
||||
)?.name
|
||||
: "Select repository"}
|
||||
)?.name ?? "Select repository")}
|
||||
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
@@ -274,11 +274,15 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
||||
placeholder="Search repository..."
|
||||
className="h-9"
|
||||
/>
|
||||
{isLoadingRepositories && (
|
||||
{!gitlabId ? (
|
||||
<span className="py-6 text-center text-sm text-muted-foreground">
|
||||
Select a GitLab account first
|
||||
</span>
|
||||
) : isLoadingRepositories ? (
|
||||
<span className="py-6 text-center text-sm">
|
||||
Loading Repositories....
|
||||
</span>
|
||||
)}
|
||||
) : null}
|
||||
<CommandEmpty>No repositories found.</CommandEmpty>
|
||||
<ScrollArea className="h-96">
|
||||
<CommandGroup>
|
||||
|
||||
@@ -430,7 +430,7 @@ export const ShowProjects = () => {
|
||||
</DropdownMenu>
|
||||
) : null}
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between gap-2">
|
||||
<CardTitle className="flex items-center justify-between gap-2 overflow-clip">
|
||||
<span className="flex flex-col gap-1.5 ">
|
||||
<div className="flex items-center gap-2">
|
||||
<BookIcon className="size-4 text-muted-foreground" />
|
||||
@@ -439,7 +439,7 @@ export const ShowProjects = () => {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span className="text-sm font-medium text-muted-foreground break-all">
|
||||
<span className="text-sm font-medium text-muted-foreground break-normal">
|
||||
{project.description}
|
||||
</span>
|
||||
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
"use client";
|
||||
|
||||
import { Link2, Loader2, Unlink } from "lucide-react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
|
||||
const LINKING_CALLBACK_URL = "/dashboard/settings/profile";
|
||||
|
||||
const TRUSTED_PROVIDERS = ["google", "github"] as const;
|
||||
type SocialProvider = (typeof TRUSTED_PROVIDERS)[number];
|
||||
|
||||
type AccountItem = {
|
||||
providerId: string;
|
||||
accountId?: string;
|
||||
};
|
||||
|
||||
function providerLabel(providerId: string): string {
|
||||
return providerId.charAt(0).toUpperCase() + providerId.slice(1);
|
||||
}
|
||||
|
||||
export function LinkingAccount() {
|
||||
const [accounts, setAccounts] = useState<AccountItem[]>([]);
|
||||
const [accountsLoading, setAccountsLoading] = useState(true);
|
||||
const [linkingProvider, setLinkingProvider] = useState<SocialProvider | null>(
|
||||
null,
|
||||
);
|
||||
const [unlinkingProviderId, setUnlinkingProviderId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const fetchAccounts = useCallback(async () => {
|
||||
setAccountsLoading(true);
|
||||
try {
|
||||
const { data } = await authClient.listAccounts();
|
||||
const list = Array.isArray(data)
|
||||
? data
|
||||
: ((data && typeof data === "object" && "accounts" in data
|
||||
? (data as { accounts?: AccountItem[] }).accounts
|
||||
: null) ?? []);
|
||||
setAccounts(Array.isArray(list) ? list : []);
|
||||
} catch {
|
||||
setAccounts([]);
|
||||
} finally {
|
||||
setAccountsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAccounts();
|
||||
}, [fetchAccounts]);
|
||||
|
||||
const linkedProviderIds = new Set(accounts.map((a) => a.providerId));
|
||||
const socialAccounts = accounts.filter((a) =>
|
||||
TRUSTED_PROVIDERS.includes(a.providerId as SocialProvider),
|
||||
);
|
||||
|
||||
const handleLinkSocial = async (provider: SocialProvider) => {
|
||||
setLinkingProvider(provider);
|
||||
try {
|
||||
const { error } = await authClient.linkSocial({
|
||||
provider,
|
||||
callbackURL: LINKING_CALLBACK_URL,
|
||||
});
|
||||
if (error) {
|
||||
toast.error(error.message ?? "Failed to link account");
|
||||
setLinkingProvider(null);
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error(
|
||||
"Failed to link account",
|
||||
err instanceof Error ? { description: err.message } : undefined,
|
||||
);
|
||||
setLinkingProvider(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUnlink = async (providerId: string, accountId?: string) => {
|
||||
setUnlinkingProviderId(providerId);
|
||||
try {
|
||||
const { error } = await authClient.unlinkAccount({
|
||||
providerId,
|
||||
...(accountId && { accountId }),
|
||||
});
|
||||
if (error) {
|
||||
toast.error(error.message ?? "Failed to unlink account");
|
||||
return;
|
||||
}
|
||||
toast.success("Account unlinked");
|
||||
await fetchAccounts();
|
||||
} catch (err) {
|
||||
toast.error(
|
||||
"Failed to unlink account",
|
||||
err instanceof Error ? { description: err.message } : undefined,
|
||||
);
|
||||
} finally {
|
||||
setUnlinkingProviderId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const canUnlink = accounts.length > 1;
|
||||
|
||||
return (
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-6xl mx-auto w-full">
|
||||
<div className="rounded-xl bg-background shadow-md">
|
||||
<CardHeader>
|
||||
<div className="flex flex-row gap-2 flex-wrap justify-between items-center">
|
||||
<div>
|
||||
<CardTitle className="text-xl flex flex-row gap-2">
|
||||
<Link2 className="size-6 text-muted-foreground self-center" />
|
||||
Linking account
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Link your Google or GitHub account to sign in with them.
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6 py-8 border-t">
|
||||
{/* Linked accounts */}
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium">Linked accounts</p>
|
||||
{accountsLoading ? (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground py-2">
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
Loading...
|
||||
</div>
|
||||
) : socialAccounts.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground py-2">
|
||||
No social accounts linked yet.
|
||||
</p>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{socialAccounts.map((acc) => (
|
||||
<li
|
||||
key={acc.accountId ?? acc.providerId}
|
||||
className="flex items-center justify-between rounded-lg border px-3 py-2 text-sm"
|
||||
>
|
||||
<span className="font-medium">
|
||||
{providerLabel(acc.providerId)}
|
||||
</span>
|
||||
{canUnlink && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||
onClick={() =>
|
||||
handleUnlink(acc.providerId, acc.accountId)
|
||||
}
|
||||
disabled={unlinkingProviderId === acc.providerId}
|
||||
isLoading={unlinkingProviderId === acc.providerId}
|
||||
>
|
||||
{unlinkingProviderId === acc.providerId ? (
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<Unlink className="mr-1.5 size-4" />
|
||||
Unlink
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Click a provider below to link it to your account. You will be
|
||||
redirected to complete the flow.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{!linkedProviderIds.has("google") && (
|
||||
<Button
|
||||
variant="outline"
|
||||
type="button"
|
||||
className="min-w-[180px]"
|
||||
onClick={() => handleLinkSocial("google")}
|
||||
disabled={!!linkingProvider}
|
||||
isLoading={linkingProvider === "google"}
|
||||
>
|
||||
{linkingProvider === "google" ? (
|
||||
<Loader2 className="mr-2 size-4 animate-spin" />
|
||||
) : (
|
||||
<svg viewBox="0 0 24 24" className="mr-2 size-4">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
Link with Google
|
||||
</Button>
|
||||
)}
|
||||
{!linkedProviderIds.has("github") && (
|
||||
<Button
|
||||
variant="outline"
|
||||
type="button"
|
||||
className="min-w-[180px]"
|
||||
onClick={() => handleLinkSocial("github")}
|
||||
disabled={!!linkingProvider}
|
||||
isLoading={linkingProvider === "github"}
|
||||
>
|
||||
{linkingProvider === "github" ? (
|
||||
<Loader2 className="mr-2 size-4 animate-spin" />
|
||||
) : (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
className="mr-2 size-4"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
|
||||
</svg>
|
||||
)}
|
||||
Link with GitHub
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -404,8 +404,7 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/license",
|
||||
icon: Key,
|
||||
// Only enabled for admins in non-cloud environments
|
||||
isEnabled: ({ auth }) =>
|
||||
!!(auth?.role === "owner" || auth?.role === "admin"),
|
||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
|
||||
@@ -166,7 +166,12 @@ export function LicenseKeySettings() {
|
||||
{!haveValidLicenseKey && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
disabled={isSaving || isValidating || isDeactivating}
|
||||
disabled={
|
||||
isSaving ||
|
||||
isValidating ||
|
||||
isDeactivating ||
|
||||
!licenseKey.trim()
|
||||
}
|
||||
isLoading={isActivating}
|
||||
onClick={async () => {
|
||||
try {
|
||||
|
||||
6
apps/dokploy/drizzle/0142_outstanding_tusk.sql
Normal file
6
apps/dokploy/drizzle/0142_outstanding_tusk.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE "application" ADD COLUMN "ulimitsSwarm" json;--> statement-breakpoint
|
||||
ALTER TABLE "mariadb" ADD COLUMN "ulimitsSwarm" json;--> statement-breakpoint
|
||||
ALTER TABLE "mongo" ADD COLUMN "ulimitsSwarm" json;--> statement-breakpoint
|
||||
ALTER TABLE "mysql" ADD COLUMN "ulimitsSwarm" json;--> statement-breakpoint
|
||||
ALTER TABLE "postgres" ADD COLUMN "ulimitsSwarm" json;--> statement-breakpoint
|
||||
ALTER TABLE "redis" ADD COLUMN "ulimitsSwarm" json;
|
||||
7284
apps/dokploy/drizzle/meta/0142_snapshot.json
Normal file
7284
apps/dokploy/drizzle/meta/0142_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -995,6 +995,13 @@
|
||||
"when": 1770490719123,
|
||||
"tag": "0141_plain_earthquake",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 142,
|
||||
"version": "7",
|
||||
"when": 1770615019498,
|
||||
"tag": "0142_outstanding_tusk",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IS_CLOUD, validateRequest } from "@dokploy/server";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import type { ReactElement } from "react";
|
||||
@@ -45,7 +45,7 @@ export async function getServerSideProps(
|
||||
},
|
||||
};
|
||||
}
|
||||
if (user.role === "member") {
|
||||
if (user.role !== "owner") {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { GetServerSidePropsContext } from "next";
|
||||
import type { ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
import { ShowApiKeys } from "@/components/dashboard/settings/api/show-api-keys";
|
||||
import { LinkingAccount } from "@/components/dashboard/settings/linking-account/linking-account";
|
||||
import { ProfileForm } from "@/components/dashboard/settings/profile/profile-form";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
@@ -12,17 +13,16 @@ import { getLocale, serverSideTranslations } from "@/utils/i18n";
|
||||
|
||||
const Page = () => {
|
||||
const { data } = api.user.get.useQuery();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
// const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
|
||||
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
|
||||
<ProfileForm />
|
||||
{isCloud && <LinkingAccount />}
|
||||
{(data?.canAccessToAPI ||
|
||||
data?.role === "owner" ||
|
||||
data?.role === "admin") && <ShowApiKeys />}
|
||||
|
||||
{/* {isCloud && <RemoveSelfAccount />} */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 4.6 KiB |
@@ -12,7 +12,7 @@ import {
|
||||
|
||||
export const licenseKeyRouter = createTRPCRouter({
|
||||
activate: adminProcedure
|
||||
.input(z.object({ licenseKey: z.string() }))
|
||||
.input(z.object({ licenseKey: z.string().min(1) }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const currentUserId = ctx.user.id;
|
||||
@@ -74,6 +74,13 @@ export const licenseKeyRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
if (ctx.user.role !== "owner") {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "You are not authorized to validate a license key",
|
||||
});
|
||||
}
|
||||
|
||||
if (!currentUser.licenseKey) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -164,6 +171,13 @@ export const licenseKeyRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
if (ctx.user.role !== "owner") {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "You are not authorized to get enterprise settings",
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
enableEnterpriseFeatures: !!currentUser.enableEnterpriseFeatures,
|
||||
licenseKey: currentUser.licenseKey ?? "",
|
||||
@@ -200,6 +214,13 @@ export const licenseKeyRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
if (ctx.user.role !== "owner") {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "You are not authorized to update enterprise settings",
|
||||
});
|
||||
}
|
||||
|
||||
await db
|
||||
.update(user)
|
||||
.set({
|
||||
|
||||
@@ -7,7 +7,12 @@ import {
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import Stripe from "stripe";
|
||||
import { z } from "zod";
|
||||
import { getStripeItems, WEBSITE_URL } from "@/server/utils/stripe";
|
||||
import {
|
||||
getStripeItems,
|
||||
PRODUCT_ANNUAL_ID,
|
||||
PRODUCT_MONTHLY_ID,
|
||||
WEBSITE_URL,
|
||||
} from "@/server/utils/stripe";
|
||||
import { adminProcedure, createTRPCRouter } from "../trpc";
|
||||
|
||||
export const stripeRouter = createTRPCRouter({
|
||||
@@ -22,6 +27,7 @@ export const stripeRouter = createTRPCRouter({
|
||||
const products = await stripe.products.list({
|
||||
expand: ["data.default_price"],
|
||||
active: true,
|
||||
ids: [PRODUCT_MONTHLY_ID, PRODUCT_ANNUAL_ID],
|
||||
});
|
||||
|
||||
if (!stripeCustomerId) {
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
import { getPublicIpWithFallback, LICENSE_KEY_URL } from "@dokploy/server";
|
||||
|
||||
const LICENSE_SERVER_UNREACHABLE =
|
||||
"Could not reach the license server. Check your connection or try again later.";
|
||||
|
||||
function isNetworkError(error: unknown): boolean {
|
||||
if (error instanceof Error) {
|
||||
if (error.message === "fetch failed") return true;
|
||||
const cause = (error as Error & { cause?: { code?: string } }).cause;
|
||||
const code = cause?.code;
|
||||
return (
|
||||
code === "ECONNREFUSED" || code === "ENOTFOUND" || code === "ETIMEDOUT"
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export const validateLicenseKey = async (licenseKey: string) => {
|
||||
try {
|
||||
const ip = await getPublicIpWithFallback();
|
||||
@@ -22,6 +37,9 @@ export const validateLicenseKey = async (licenseKey: string) => {
|
||||
console.error(
|
||||
error instanceof Error ? error.message : "Failed to validate license key",
|
||||
);
|
||||
if (isNetworkError(error)) {
|
||||
throw new Error(LICENSE_SERVER_UNREACHABLE);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -48,6 +66,9 @@ export const activateLicenseKey = async (licenseKey: string) => {
|
||||
console.error(
|
||||
error instanceof Error ? error.message : "Failed to activate license key",
|
||||
);
|
||||
if (isNetworkError(error)) {
|
||||
throw new Error(LICENSE_SERVER_UNREACHABLE);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -76,6 +97,9 @@ export const deactivateLicenseKey = async (licenseKey: string) => {
|
||||
? error.message
|
||||
: "Failed to deactivate license key",
|
||||
);
|
||||
if (isNetworkError(error)) {
|
||||
throw new Error(LICENSE_SERVER_UNREACHABLE);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,9 +3,12 @@ export const WEBSITE_URL =
|
||||
? "http://localhost:3000"
|
||||
: process.env.SITE_URL;
|
||||
|
||||
const BASE_PRICE_MONTHLY_ID = process.env.BASE_PRICE_MONTHLY_ID!; // $4.00
|
||||
export const BASE_PRICE_MONTHLY_ID = process.env.BASE_PRICE_MONTHLY_ID!; // $4.00
|
||||
|
||||
const BASE_ANNUAL_MONTHLY_ID = process.env.BASE_ANNUAL_MONTHLY_ID!; // $7.99
|
||||
export const BASE_ANNUAL_MONTHLY_ID = process.env.BASE_ANNUAL_MONTHLY_ID!; // $7.99
|
||||
|
||||
export const PRODUCT_MONTHLY_ID = process.env.PRODUCT_MONTHLY_ID!;
|
||||
export const PRODUCT_ANNUAL_ID = process.env.PRODUCT_ANNUAL_ID!;
|
||||
|
||||
export const getStripeItems = (serverQuantity: number, isAnnual: boolean) => {
|
||||
const items = [];
|
||||
|
||||
@@ -43,6 +43,8 @@ import {
|
||||
type ServiceModeSwarm,
|
||||
ServiceModeSwarmSchema,
|
||||
triggerType,
|
||||
type UlimitsSwarm,
|
||||
UlimitsSwarmSchema,
|
||||
type UpdateConfigSwarm,
|
||||
UpdateConfigSwarmSchema,
|
||||
} from "./shared";
|
||||
@@ -172,6 +174,7 @@ export const applications = pgTable("application", {
|
||||
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
|
||||
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
|
||||
endpointSpecSwarm: json("endpointSpecSwarm").$type<EndpointSpecSwarm>(),
|
||||
ulimitsSwarm: json("ulimitsSwarm").$type<UlimitsSwarm>(),
|
||||
//
|
||||
replicas: integer("replicas").default(1).notNull(),
|
||||
applicationStatus: applicationStatus("applicationStatus")
|
||||
@@ -364,6 +367,7 @@ const createSchema = createInsertSchema(applications, {
|
||||
cleanCache: z.boolean().optional(),
|
||||
stopGracePeriodSwarm: z.bigint().nullable(),
|
||||
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
|
||||
ulimitsSwarm: UlimitsSwarmSchema.nullable(),
|
||||
});
|
||||
|
||||
export const apiCreateApplication = createSchema.pick({
|
||||
|
||||
@@ -23,6 +23,8 @@ import {
|
||||
RestartPolicySwarmSchema,
|
||||
type ServiceModeSwarm,
|
||||
ServiceModeSwarmSchema,
|
||||
type UlimitsSwarm,
|
||||
UlimitsSwarmSchema,
|
||||
type UpdateConfigSwarm,
|
||||
UpdateConfigSwarmSchema,
|
||||
} from "./shared";
|
||||
@@ -67,6 +69,7 @@ export const mariadb = pgTable("mariadb", {
|
||||
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
|
||||
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
|
||||
endpointSpecSwarm: json("endpointSpecSwarm").$type<EndpointSpecSwarm>(),
|
||||
ulimitsSwarm: json("ulimitsSwarm").$type<UlimitsSwarm>(),
|
||||
replicas: integer("replicas").default(1).notNull(),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
@@ -141,6 +144,7 @@ const createSchema = createInsertSchema(mariadb, {
|
||||
networkSwarm: NetworkSwarmSchema.nullable(),
|
||||
stopGracePeriodSwarm: z.bigint().nullable(),
|
||||
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
|
||||
ulimitsSwarm: UlimitsSwarmSchema.nullable(),
|
||||
});
|
||||
|
||||
export const apiCreateMariaDB = createSchema.pick({
|
||||
|
||||
@@ -30,6 +30,8 @@ import {
|
||||
RestartPolicySwarmSchema,
|
||||
type ServiceModeSwarm,
|
||||
ServiceModeSwarmSchema,
|
||||
type UlimitsSwarm,
|
||||
UlimitsSwarmSchema,
|
||||
type UpdateConfigSwarm,
|
||||
UpdateConfigSwarmSchema,
|
||||
} from "./shared";
|
||||
@@ -70,6 +72,7 @@ export const mongo = pgTable("mongo", {
|
||||
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
|
||||
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
|
||||
endpointSpecSwarm: json("endpointSpecSwarm").$type<EndpointSpecSwarm>(),
|
||||
ulimitsSwarm: json("ulimitsSwarm").$type<UlimitsSwarm>(),
|
||||
replicas: integer("replicas").default(1).notNull(),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
@@ -138,6 +141,7 @@ const createSchema = createInsertSchema(mongo, {
|
||||
networkSwarm: NetworkSwarmSchema.nullable(),
|
||||
stopGracePeriodSwarm: z.bigint().nullable(),
|
||||
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
|
||||
ulimitsSwarm: UlimitsSwarmSchema.nullable(),
|
||||
});
|
||||
|
||||
export const apiCreateMongo = createSchema.pick({
|
||||
|
||||
@@ -23,6 +23,8 @@ import {
|
||||
RestartPolicySwarmSchema,
|
||||
type ServiceModeSwarm,
|
||||
ServiceModeSwarmSchema,
|
||||
type UlimitsSwarm,
|
||||
UlimitsSwarmSchema,
|
||||
type UpdateConfigSwarm,
|
||||
UpdateConfigSwarmSchema,
|
||||
} from "./shared";
|
||||
@@ -65,6 +67,7 @@ export const mysql = pgTable("mysql", {
|
||||
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
|
||||
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
|
||||
endpointSpecSwarm: json("endpointSpecSwarm").$type<EndpointSpecSwarm>(),
|
||||
ulimitsSwarm: json("ulimitsSwarm").$type<UlimitsSwarm>(),
|
||||
replicas: integer("replicas").default(1).notNull(),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
@@ -138,6 +141,7 @@ const createSchema = createInsertSchema(mysql, {
|
||||
networkSwarm: NetworkSwarmSchema.nullable(),
|
||||
stopGracePeriodSwarm: z.bigint().nullable(),
|
||||
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
|
||||
ulimitsSwarm: UlimitsSwarmSchema.nullable(),
|
||||
});
|
||||
|
||||
export const apiCreateMySql = createSchema.pick({
|
||||
|
||||
@@ -23,6 +23,8 @@ import {
|
||||
RestartPolicySwarmSchema,
|
||||
type ServiceModeSwarm,
|
||||
ServiceModeSwarmSchema,
|
||||
type UlimitsSwarm,
|
||||
UlimitsSwarmSchema,
|
||||
type UpdateConfigSwarm,
|
||||
UpdateConfigSwarmSchema,
|
||||
} from "./shared";
|
||||
@@ -65,6 +67,7 @@ export const postgres = pgTable("postgres", {
|
||||
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
|
||||
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
|
||||
endpointSpecSwarm: json("endpointSpecSwarm").$type<EndpointSpecSwarm>(),
|
||||
ulimitsSwarm: json("ulimitsSwarm").$type<UlimitsSwarm>(),
|
||||
replicas: integer("replicas").default(1).notNull(),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
@@ -132,6 +135,7 @@ const createSchema = createInsertSchema(postgres, {
|
||||
networkSwarm: NetworkSwarmSchema.nullable(),
|
||||
stopGracePeriodSwarm: z.bigint().nullable(),
|
||||
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
|
||||
ulimitsSwarm: UlimitsSwarmSchema.nullable(),
|
||||
});
|
||||
|
||||
export const apiCreatePostgres = createSchema.pick({
|
||||
|
||||
@@ -22,6 +22,8 @@ import {
|
||||
RestartPolicySwarmSchema,
|
||||
type ServiceModeSwarm,
|
||||
ServiceModeSwarmSchema,
|
||||
type UlimitsSwarm,
|
||||
UlimitsSwarmSchema,
|
||||
type UpdateConfigSwarm,
|
||||
UpdateConfigSwarmSchema,
|
||||
} from "./shared";
|
||||
@@ -64,6 +66,7 @@ export const redis = pgTable("redis", {
|
||||
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
|
||||
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
|
||||
endpointSpecSwarm: json("endpointSpecSwarm").$type<EndpointSpecSwarm>(),
|
||||
ulimitsSwarm: json("ulimitsSwarm").$type<UlimitsSwarm>(),
|
||||
replicas: integer("replicas").default(1).notNull(),
|
||||
|
||||
environmentId: text("environmentId")
|
||||
@@ -120,6 +123,7 @@ const createSchema = createInsertSchema(redis, {
|
||||
networkSwarm: NetworkSwarmSchema.nullable(),
|
||||
stopGracePeriodSwarm: z.bigint().nullable(),
|
||||
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
|
||||
ulimitsSwarm: UlimitsSwarmSchema.nullable(),
|
||||
});
|
||||
|
||||
export const apiCreateRedis = createSchema.pick({
|
||||
|
||||
@@ -86,6 +86,14 @@ export interface EndpointSpecSwarm {
|
||||
Ports?: EndpointPortConfigSwarm[] | undefined;
|
||||
}
|
||||
|
||||
export interface UlimitSwarm {
|
||||
Name: string;
|
||||
Soft: number;
|
||||
Hard: number;
|
||||
}
|
||||
|
||||
export type UlimitsSwarm = UlimitSwarm[];
|
||||
|
||||
export const HealthCheckSwarmSchema = z
|
||||
.object({
|
||||
Test: z.array(z.string()).optional(),
|
||||
@@ -189,3 +197,13 @@ export const EndpointSpecSwarmSchema = z
|
||||
Ports: z.array(EndpointPortConfigSwarmSchema).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const UlimitSwarmSchema = z
|
||||
.object({
|
||||
Name: z.string().min(1),
|
||||
Soft: z.number().int().min(-1),
|
||||
Hard: z.number().int().min(-1),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const UlimitsSwarmSchema = z.array(UlimitSwarmSchema);
|
||||
|
||||
@@ -43,6 +43,17 @@ const { handler, api } = betterAuth({
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(IS_CLOUD
|
||||
? {
|
||||
account: {
|
||||
accountLinking: {
|
||||
enabled: true,
|
||||
trustedProviders: ["github", "google"],
|
||||
allowDifferentEmails: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
appName: "Dokploy",
|
||||
socialProviders: {
|
||||
github: {
|
||||
|
||||
@@ -220,6 +220,7 @@ const rollbackApplication = async (
|
||||
RollbackConfig,
|
||||
UpdateConfig,
|
||||
Networks,
|
||||
Ulimits,
|
||||
} = generateConfigContainer(fullContext as ApplicationNested);
|
||||
|
||||
const bindsMount = generateBindMounts(mounts);
|
||||
@@ -254,6 +255,7 @@ const rollbackApplication = async (
|
||||
Args: ["-c", command],
|
||||
}
|
||||
: {}),
|
||||
...(Ulimits && { Ulimits }),
|
||||
Labels,
|
||||
},
|
||||
Networks,
|
||||
|
||||
@@ -88,7 +88,7 @@ export const createCommand = (compose: ComposeNested) => {
|
||||
let command = "";
|
||||
|
||||
if (composeType === "docker-compose") {
|
||||
command = `compose -p ${appName} -f ${path} up -d --build --remove-orphans`;
|
||||
command = `compose -p ${appName} -f ${path} up -d --build --pull always --remove-orphans`;
|
||||
} else if (composeType === "stack") {
|
||||
command = `stack deploy -c ${path} ${appName} --prune --with-registry-auth`;
|
||||
}
|
||||
|
||||
@@ -110,6 +110,7 @@ export const mechanizeDockerContainer = async (
|
||||
Networks,
|
||||
StopGracePeriod,
|
||||
EndpointSpec,
|
||||
Ulimits,
|
||||
} = generateConfigContainer(application);
|
||||
|
||||
const bindsMount = generateBindMounts(mounts);
|
||||
@@ -142,7 +143,7 @@ export const mechanizeDockerContainer = async (
|
||||
args.length > 0 && {
|
||||
Args: args,
|
||||
}),
|
||||
|
||||
...(Ulimits && { Ulimits }),
|
||||
Labels,
|
||||
},
|
||||
Networks,
|
||||
|
||||
@@ -7,7 +7,7 @@ import { user as userSchema } from "../../db/schema/user";
|
||||
export const LICENSE_KEY_URL =
|
||||
process.env.NODE_ENV === "development"
|
||||
? "http://localhost:4002"
|
||||
: "https://licenses.dokploy.com";
|
||||
: "https://licenses-api.dokploy.com";
|
||||
|
||||
export const initEnterpriseBackupCronJobs = async () => {
|
||||
scheduleJob("enterprise-check", "0 0 */3 * *", async () => {
|
||||
|
||||
@@ -48,6 +48,7 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
|
||||
Networks,
|
||||
StopGracePeriod,
|
||||
EndpointSpec,
|
||||
Ulimits,
|
||||
} = generateConfigContainer(mariadb);
|
||||
const resources = calculateResources({
|
||||
memoryLimit,
|
||||
@@ -83,7 +84,7 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
|
||||
args.length > 0 && {
|
||||
Args: args,
|
||||
}),
|
||||
|
||||
...(Ulimits && { Ulimits }),
|
||||
Labels,
|
||||
},
|
||||
Networks,
|
||||
|
||||
@@ -94,6 +94,7 @@ ${command ?? "wait $MONGOD_PID"}`;
|
||||
Networks,
|
||||
StopGracePeriod,
|
||||
EndpointSpec,
|
||||
Ulimits,
|
||||
} = generateConfigContainer(mongo);
|
||||
|
||||
const resources = calculateResources({
|
||||
@@ -139,7 +140,7 @@ ${command ?? "wait $MONGOD_PID"}`;
|
||||
!replicaSets && {
|
||||
Args: args,
|
||||
}),
|
||||
|
||||
...(Ulimits && { Ulimits }),
|
||||
Labels,
|
||||
},
|
||||
Networks,
|
||||
|
||||
@@ -54,6 +54,7 @@ export const buildMysql = async (mysql: MysqlNested) => {
|
||||
Networks,
|
||||
StopGracePeriod,
|
||||
EndpointSpec,
|
||||
Ulimits,
|
||||
} = generateConfigContainer(mysql);
|
||||
const resources = calculateResources({
|
||||
memoryLimit,
|
||||
@@ -89,7 +90,7 @@ export const buildMysql = async (mysql: MysqlNested) => {
|
||||
args.length > 0 && {
|
||||
Args: args,
|
||||
}),
|
||||
|
||||
...(Ulimits && { Ulimits }),
|
||||
Labels,
|
||||
},
|
||||
Networks,
|
||||
|
||||
@@ -47,6 +47,7 @@ export const buildPostgres = async (postgres: PostgresNested) => {
|
||||
Networks,
|
||||
StopGracePeriod,
|
||||
EndpointSpec,
|
||||
Ulimits,
|
||||
} = generateConfigContainer(postgres);
|
||||
const resources = calculateResources({
|
||||
memoryLimit,
|
||||
@@ -82,7 +83,7 @@ export const buildPostgres = async (postgres: PostgresNested) => {
|
||||
args.length > 0 && {
|
||||
Args: args,
|
||||
}),
|
||||
|
||||
...(Ulimits && { Ulimits }),
|
||||
Labels,
|
||||
},
|
||||
Networks,
|
||||
|
||||
@@ -45,6 +45,7 @@ export const buildRedis = async (redis: RedisNested) => {
|
||||
Networks,
|
||||
StopGracePeriod,
|
||||
EndpointSpec,
|
||||
Ulimits,
|
||||
} = generateConfigContainer(redis);
|
||||
const resources = calculateResources({
|
||||
memoryLimit,
|
||||
@@ -87,6 +88,7 @@ export const buildRedis = async (redis: RedisNested) => {
|
||||
Command: ["/bin/sh"],
|
||||
Args: ["-c", `redis-server --requirepass ${databasePassword}`],
|
||||
}),
|
||||
...(Ulimits && { Ulimits }),
|
||||
Labels,
|
||||
},
|
||||
Networks,
|
||||
|
||||
@@ -508,6 +508,7 @@ export const generateConfigContainer = (
|
||||
networkSwarm,
|
||||
stopGracePeriodSwarm,
|
||||
endpointSpecSwarm,
|
||||
ulimitsSwarm,
|
||||
} = application;
|
||||
|
||||
const sanitizedStopGracePeriodSwarm =
|
||||
@@ -584,6 +585,10 @@ export const generateConfigContainer = (
|
||||
})) || [],
|
||||
},
|
||||
}),
|
||||
...(ulimitsSwarm &&
|
||||
ulimitsSwarm.length > 0 && {
|
||||
Ulimits: ulimitsSwarm,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user