mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-17 13:15:23 +02:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f5c2dbe92 | ||
|
|
0f9505327f | ||
|
|
dd2902a57c | ||
|
|
0138a7c011 | ||
|
|
845d2a3ac5 | ||
|
|
4033bb84b2 | ||
|
|
43e96edcdd | ||
|
|
2db388536f | ||
|
|
43876efc79 | ||
|
|
e7c7545c02 | ||
|
|
77705381cd | ||
|
|
5fdf82a27f | ||
|
|
6bd5b1f71f | ||
|
|
17d6830b66 | ||
|
|
a845eba320 | ||
|
|
2f4ec9f35f | ||
|
|
b725861b55 | ||
|
|
6fa8f63277 | ||
|
|
ac6bdf60ec | ||
|
|
db292e6949 | ||
|
|
085f6bbbb7 | ||
|
|
cbdc4e4a20 | ||
|
|
ee3ff18feb | ||
|
|
f5084dd5fb | ||
|
|
1b603d84d7 | ||
|
|
cf2c89d136 | ||
|
|
95de98e94d | ||
|
|
4416ca9cd2 |
@@ -7,7 +7,7 @@ import {
|
||||
RefreshCw,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { type Control, useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
@@ -57,6 +57,7 @@ export const commonCronExpressions = [
|
||||
{ label: "Every month on the 1st at midnight", value: "0 0 1 * *" },
|
||||
{ label: "Every 15 minutes", value: "*/15 * * * *" },
|
||||
{ label: "Every weekday at midnight", value: "0 0 * * 1-5" },
|
||||
{ label: "Custom", value: "custom" },
|
||||
];
|
||||
|
||||
const formSchema = z
|
||||
@@ -115,10 +116,91 @@ interface Props {
|
||||
scheduleType?: "application" | "compose" | "server" | "dokploy-server";
|
||||
}
|
||||
|
||||
export const ScheduleFormField = ({
|
||||
name,
|
||||
formControl,
|
||||
}: {
|
||||
name: string;
|
||||
formControl: Control<any>;
|
||||
}) => {
|
||||
const [selectedOption, setSelectedOption] = useState("");
|
||||
|
||||
return (
|
||||
<FormField
|
||||
control={formControl}
|
||||
name={name}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex items-center gap-2">
|
||||
Schedule
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Cron expression format: minute hour day month weekday</p>
|
||||
<p>Example: 0 0 * * * (daily at midnight)</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</FormLabel>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Select
|
||||
value={selectedOption}
|
||||
onValueChange={(value) => {
|
||||
setSelectedOption(value);
|
||||
field.onChange(value === "custom" ? "" : value);
|
||||
}}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a predefined schedule" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{commonCronExpressions.map((expr) => (
|
||||
<SelectItem key={expr.value} value={expr.value}>
|
||||
{expr.label}
|
||||
{expr.value !== "custom" && ` (${expr.value})`}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="relative">
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Custom cron expression (e.g., 0 0 * * *)"
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
const commonExpression = commonCronExpressions.find(
|
||||
(expression) => expression.value === value,
|
||||
);
|
||||
if (commonExpression) {
|
||||
setSelectedOption(commonExpression.value);
|
||||
} else {
|
||||
setSelectedOption("custom");
|
||||
}
|
||||
field.onChange(e);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<FormDescription>
|
||||
Choose a predefined schedule or enter a custom cron expression
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [cacheType, setCacheType] = useState<CacheType>("cache");
|
||||
|
||||
const utils = api.useUtils();
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
@@ -377,63 +459,9 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
<ScheduleFormField
|
||||
name="cronExpression"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex items-center gap-2">
|
||||
Schedule
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Cron expression format: minute hour day month
|
||||
weekday
|
||||
</p>
|
||||
<p>Example: 0 0 * * * (daily at midnight)</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</FormLabel>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
}}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a predefined schedule" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{commonCronExpressions.map((expr) => (
|
||||
<SelectItem key={expr.value} value={expr.value}>
|
||||
{expr.label} ({expr.value})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="relative">
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Custom cron expression (e.g., 0 0 * * *)"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<FormDescription>
|
||||
Choose a predefined schedule or enter a custom cron
|
||||
expression
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
formControl={form.control}
|
||||
/>
|
||||
|
||||
{(scheduleTypeForm === "application" ||
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import {
|
||||
DatabaseZap,
|
||||
Info,
|
||||
PenBoxIcon,
|
||||
PlusCircle,
|
||||
RefreshCw,
|
||||
} from "lucide-react";
|
||||
import { DatabaseZap, PenBoxIcon, PlusCircle, RefreshCw } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -47,7 +41,7 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import type { CacheType } from "../domains/handle-domain";
|
||||
import { commonCronExpressions } from "../schedules/handle-schedules";
|
||||
import { ScheduleFormField } from "../schedules/handle-schedules";
|
||||
|
||||
const formSchema = z
|
||||
.object({
|
||||
@@ -306,64 +300,9 @@ export const HandleVolumeBackups = ({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
<ScheduleFormField
|
||||
name="cronExpression"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex items-center gap-2">
|
||||
Schedule
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Cron expression format: minute hour day month
|
||||
weekday
|
||||
</p>
|
||||
<p>Example: 0 0 * * * (daily at midnight)</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</FormLabel>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
}}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a predefined schedule" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{commonCronExpressions.map((expr) => (
|
||||
<SelectItem key={expr.value} value={expr.value}>
|
||||
{expr.label} ({expr.value})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="relative">
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Custom cron expression (e.g., 0 0 * * *)"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<FormDescription>
|
||||
Choose a predefined schedule or enter a custom cron
|
||||
expression
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
formControl={form.control}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
@@ -35,6 +35,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
|
||||
);
|
||||
|
||||
const { mutateAsync, isLoading } = api.compose.update.useMutation();
|
||||
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
||||
|
||||
const form = useForm<AddComposeFile>({
|
||||
defaultValues: {
|
||||
@@ -53,6 +54,12 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
|
||||
}
|
||||
}, [form, form.reset, data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.composeFile !== undefined) {
|
||||
setHasUnsavedChanges(composeFile !== data.composeFile);
|
||||
}
|
||||
}, [composeFile, data?.composeFile]);
|
||||
|
||||
const onSubmit = async (data: AddComposeFile) => {
|
||||
const { valid, error } = validateAndFormatYAML(data.composeFile);
|
||||
if (!valid) {
|
||||
@@ -71,6 +78,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Compose config Updated");
|
||||
setHasUnsavedChanges(false);
|
||||
refetch();
|
||||
await utils.compose.getConvertedCompose.invalidate({
|
||||
composeId,
|
||||
@@ -99,6 +107,19 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<div className="w-full flex flex-col gap-4 ">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium">Compose File</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Configure your Docker Compose file for this service.
|
||||
{hasUnsavedChanges && (
|
||||
<span className="text-yellow-500 ml-2">
|
||||
(You have unsaved changes)
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form-save-compose-file"
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
CheckIcon,
|
||||
ChevronsUpDown,
|
||||
DatabaseZap,
|
||||
Info,
|
||||
PenBoxIcon,
|
||||
PlusIcon,
|
||||
RefreshCw,
|
||||
@@ -62,7 +61,7 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { commonCronExpressions } from "../../application/schedules/handle-schedules";
|
||||
import { ScheduleFormField } from "../../application/schedules/handle-schedules";
|
||||
|
||||
type CacheType = "cache" | "fetch";
|
||||
|
||||
@@ -579,66 +578,9 @@ export const HandleBackup = ({
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="schedule"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel className="flex items-center gap-2">
|
||||
Schedule
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Cron expression format: minute hour day month
|
||||
weekday
|
||||
</p>
|
||||
<p>Example: 0 0 * * * (daily at midnight)</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</FormLabel>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
}}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a predefined schedule" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{commonCronExpressions.map((expr) => (
|
||||
<SelectItem key={expr.value} value={expr.value}>
|
||||
{expr.label} ({expr.value})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="relative">
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Custom cron expression (e.g., 0 0 * * *)"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<FormDescription>
|
||||
Choose a predefined schedule or enter a custom cron
|
||||
expression
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<ScheduleFormField name="schedule" formControl={form.control} />
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="prefix"
|
||||
|
||||
@@ -96,8 +96,30 @@ export const ShowProjects = () => {
|
||||
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
||||
break;
|
||||
case "services": {
|
||||
const aTotalServices = a.environments.length;
|
||||
const bTotalServices = b.environments.length;
|
||||
const aTotalServices = a.environments.reduce((total, env) => {
|
||||
return (
|
||||
total +
|
||||
(env.applications?.length || 0) +
|
||||
(env.mariadb?.length || 0) +
|
||||
(env.mongo?.length || 0) +
|
||||
(env.mysql?.length || 0) +
|
||||
(env.postgres?.length || 0) +
|
||||
(env.redis?.length || 0) +
|
||||
(env.compose?.length || 0)
|
||||
);
|
||||
}, 0);
|
||||
const bTotalServices = b.environments.reduce((total, env) => {
|
||||
return (
|
||||
total +
|
||||
(env.applications?.length || 0) +
|
||||
(env.mariadb?.length || 0) +
|
||||
(env.mongo?.length || 0) +
|
||||
(env.mysql?.length || 0) +
|
||||
(env.postgres?.length || 0) +
|
||||
(env.redis?.length || 0) +
|
||||
(env.compose?.length || 0)
|
||||
);
|
||||
}, 0);
|
||||
comparison = aTotalServices - bTotalServices;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,14 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Props {
|
||||
status: "running" | "error" | "done" | "idle" | undefined | null;
|
||||
status:
|
||||
| "running"
|
||||
| "error"
|
||||
| "done"
|
||||
| "idle"
|
||||
| "cancelled"
|
||||
| undefined
|
||||
| null;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
@@ -34,6 +41,14 @@ export const StatusTooltip = ({ status, className }: Props) => {
|
||||
className={cn("size-3.5 rounded-full bg-green-500", className)}
|
||||
/>
|
||||
)}
|
||||
{status === "cancelled" && (
|
||||
<div
|
||||
className={cn(
|
||||
"size-3.5 rounded-full bg-muted-foreground",
|
||||
className,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{status === "running" && (
|
||||
<div
|
||||
className={cn("size-3.5 rounded-full bg-yellow-500", className)}
|
||||
@@ -46,6 +61,7 @@ export const StatusTooltip = ({ status, className }: Props) => {
|
||||
{status === "error" && "Error"}
|
||||
{status === "done" && "Done"}
|
||||
{status === "running" && "Running"}
|
||||
{status === "cancelled" && "Cancelled"}
|
||||
</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
1
apps/dokploy/drizzle/0113_complete_rafael_vega.sql
Normal file
1
apps/dokploy/drizzle/0113_complete_rafael_vega.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TYPE "public"."deploymentStatus" ADD VALUE 'cancelled';
|
||||
6572
apps/dokploy/drizzle/meta/0113_snapshot.json
Normal file
6572
apps/dokploy/drizzle/meta/0113_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -792,6 +792,13 @@
|
||||
"when": 1758483520214,
|
||||
"tag": "0112_freezing_skrulls",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 113,
|
||||
"version": "7",
|
||||
"when": 1758960816504,
|
||||
"tag": "0113_complete_rafael_vega",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dokploy",
|
||||
"version": "v0.25.3",
|
||||
"version": "v0.25.4",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -226,6 +226,7 @@ const Service = (
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||
<TabsTrigger value="domains">Domains</TabsTrigger>
|
||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||
<TabsTrigger value="preview-deployments">
|
||||
Preview Deployments
|
||||
</TabsTrigger>
|
||||
@@ -233,7 +234,6 @@ const Service = (
|
||||
<TabsTrigger value="volume-backups">
|
||||
Volume Backups
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
{((data?.serverId && isCloud) || !data?.server) && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
initializeNetwork,
|
||||
initSchedules,
|
||||
initVolumeBackupsCronJobs,
|
||||
initCancelDeployments,
|
||||
sendDokployRestartNotifications,
|
||||
setupDirectories,
|
||||
} from "@dokploy/server";
|
||||
@@ -52,6 +53,7 @@ void app.prepare().then(async () => {
|
||||
await migration();
|
||||
await initCronJobs();
|
||||
await initSchedules();
|
||||
await initCancelDeployments();
|
||||
await initVolumeBackupsCronJobs();
|
||||
await sendDokployRestartNotifications();
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ export const deploymentStatus = pgEnum("deploymentStatus", [
|
||||
"running",
|
||||
"done",
|
||||
"error",
|
||||
"cancelled",
|
||||
]);
|
||||
|
||||
export const deployments = pgTable("deployment", {
|
||||
|
||||
@@ -68,6 +68,7 @@ export * from "./utils/backups/postgres";
|
||||
export * from "./utils/backups/utils";
|
||||
export * from "./utils/backups/web-server";
|
||||
export * from "./utils/builders/compose";
|
||||
export * from "./utils/startup/cancell-deployments";
|
||||
export * from "./utils/builders/docker-file";
|
||||
export * from "./utils/builders/drop";
|
||||
export * from "./utils/builders/heroku";
|
||||
|
||||
@@ -603,6 +603,21 @@ const BUNNY_CDN_IPS = new Set([
|
||||
"89.187.184.176",
|
||||
]);
|
||||
|
||||
// Arvancloud IP ranges
|
||||
// https://www.arvancloud.ir/fa/ips.txt
|
||||
const ARVANCLOUD_IP_RANGES = [
|
||||
"185.143.232.0/22",
|
||||
"188.229.116.16/29",
|
||||
"94.101.182.0/27",
|
||||
"2.144.3.128/28",
|
||||
"89.45.48.64/28",
|
||||
"37.32.16.0/27",
|
||||
"37.32.17.0/27",
|
||||
"37.32.18.0/27",
|
||||
"37.32.19.0/27",
|
||||
"185.215.232.0/22",
|
||||
];
|
||||
|
||||
const CDN_PROVIDERS: CDNProvider[] = [
|
||||
{
|
||||
name: "cloudflare",
|
||||
@@ -627,6 +642,14 @@ const CDN_PROVIDERS: CDNProvider[] = [
|
||||
warningMessage:
|
||||
"Domain is behind Fastly - actual IP is masked by CDN proxy",
|
||||
},
|
||||
{
|
||||
name: "arvancloud",
|
||||
displayName: "Arvancloud",
|
||||
checkIp: (ip: string) =>
|
||||
ARVANCLOUD_IP_RANGES.some((range) => isIPInCIDR(ip, range)),
|
||||
warningMessage:
|
||||
"Domain is behind Arvancloud - actual IP is masked by CDN proxy",
|
||||
},
|
||||
];
|
||||
|
||||
export const detectCDNProvider = (ip: string): CDNProvider | null => {
|
||||
|
||||
@@ -227,7 +227,7 @@ export const deployCompose = async ({
|
||||
|
||||
const buildLink = `${await getDokployUrl()}/dashboard/project/${
|
||||
compose.environment.projectId
|
||||
}/services/compose/${compose.composeId}?tab=deployments`;
|
||||
}/environment/${compose.environmentId}/services/compose/${compose.composeId}?tab=deployments`;
|
||||
const deployment = await createDeploymentCompose({
|
||||
composeId: composeId,
|
||||
title: titleLog,
|
||||
@@ -335,7 +335,7 @@ export const deployRemoteCompose = async ({
|
||||
|
||||
const buildLink = `${await getDokployUrl()}/dashboard/project/${
|
||||
compose.environment.projectId
|
||||
}/services/compose/${compose.composeId}?tab=deployments`;
|
||||
}/environment/${compose.environmentId}/services/compose/${compose.composeId}?tab=deployments`;
|
||||
const deployment = await createDeploymentCompose({
|
||||
composeId: composeId,
|
||||
title: titleLog,
|
||||
|
||||
@@ -31,29 +31,51 @@ export const getBitbucketCloneUrl = (
|
||||
apiToken?: string | null;
|
||||
bitbucketUsername?: string | null;
|
||||
appPassword?: string | null;
|
||||
bitbucketEmail?: string | null;
|
||||
bitbucketWorkspaceName?: string | null;
|
||||
} | null,
|
||||
repoClone: string,
|
||||
) => {
|
||||
if (!bitbucketProvider) {
|
||||
throw new Error("Bitbucket provider is required");
|
||||
}
|
||||
return bitbucketProvider.apiToken
|
||||
? `https://x-token-auth:${bitbucketProvider.apiToken}@${repoClone}`
|
||||
: `https://${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}@${repoClone}`;
|
||||
|
||||
if (bitbucketProvider.apiToken) {
|
||||
return `https://x-bitbucket-api-token-auth:${bitbucketProvider.apiToken}@${repoClone}`;
|
||||
}
|
||||
|
||||
// For app passwords, use username:app_password format
|
||||
if (!bitbucketProvider.bitbucketUsername || !bitbucketProvider.appPassword) {
|
||||
throw new Error(
|
||||
"Username and app password are required when not using API token",
|
||||
);
|
||||
}
|
||||
return `https://${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}@${repoClone}`;
|
||||
};
|
||||
|
||||
export const getBitbucketHeaders = (bitbucketProvider: Bitbucket) => {
|
||||
if (bitbucketProvider.apiToken) {
|
||||
// For API tokens, use HTTP Basic auth with email and token
|
||||
// According to Bitbucket docs: email:token for API calls
|
||||
const email =
|
||||
bitbucketProvider.bitbucketEmail || bitbucketProvider.bitbucketUsername;
|
||||
// According to Bitbucket official docs, for API calls with API tokens:
|
||||
// "You will need both your Atlassian account email and an API token"
|
||||
// Use: {atlassian_account_email}:{api_token}
|
||||
|
||||
if (!bitbucketProvider.bitbucketEmail) {
|
||||
throw new Error(
|
||||
"Atlassian account email is required when using API token for API calls",
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
Authorization: `Basic ${Buffer.from(`${email}:${bitbucketProvider.apiToken}`).toString("base64")}`,
|
||||
Authorization: `Basic ${Buffer.from(`${bitbucketProvider.bitbucketEmail}:${bitbucketProvider.apiToken}`).toString("base64")}`,
|
||||
};
|
||||
}
|
||||
|
||||
// For app passwords, use HTTP Basic auth with username and app password
|
||||
if (!bitbucketProvider.bitbucketUsername || !bitbucketProvider.appPassword) {
|
||||
throw new Error(
|
||||
"Username and app password are required when not using API token",
|
||||
);
|
||||
}
|
||||
return {
|
||||
Authorization: `Basic ${Buffer.from(`${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}`).toString("base64")}`,
|
||||
};
|
||||
|
||||
@@ -99,6 +99,19 @@ export const refreshGiteaToken = async (giteaProviderId: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const buildGiteaCloneUrl = (
|
||||
giteaUrl: string,
|
||||
accessToken: string,
|
||||
owner: string,
|
||||
repository: string,
|
||||
) => {
|
||||
const protocol = giteaUrl.startsWith("http://") ? "http" : "https";
|
||||
const baseUrl = giteaUrl.replace(/^https?:\/\//, "");
|
||||
const repoClone = `${owner}/${repository}.git`;
|
||||
const cloneUrl = `${protocol}://oauth2:${accessToken}@${baseUrl}/${repoClone}`;
|
||||
return cloneUrl;
|
||||
};
|
||||
|
||||
export type ApplicationWithGitea = InferResultType<
|
||||
"applications",
|
||||
{ gitea: true }
|
||||
@@ -148,9 +161,13 @@ export const getGiteaCloneCommand = async (
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
|
||||
const baseUrl = gitea?.giteaUrl.replace(/^https?:\/\//, "");
|
||||
const repoClone = `${giteaOwner}/${giteaRepository}.git`;
|
||||
const cloneUrl = `https://oauth2:${gitea?.accessToken}@${baseUrl}/${repoClone}`;
|
||||
const cloneUrl = buildGiteaCloneUrl(
|
||||
gitea?.giteaUrl!,
|
||||
gitea?.accessToken!,
|
||||
giteaOwner!,
|
||||
giteaRepository!,
|
||||
);
|
||||
|
||||
const cloneCommand = `
|
||||
rm -rf ${outputPath};
|
||||
@@ -205,8 +222,12 @@ export const cloneGiteaRepository = async (
|
||||
await recreateDirectory(outputPath);
|
||||
|
||||
const repoClone = `${giteaOwner}/${giteaRepository}.git`;
|
||||
const baseUrl = giteaProvider.giteaUrl.replace(/^https?:\/\//, "");
|
||||
const cloneUrl = `https://oauth2:${giteaProvider.accessToken}@${baseUrl}/${repoClone}`;
|
||||
const cloneUrl = buildGiteaCloneUrl(
|
||||
giteaProvider.giteaUrl,
|
||||
giteaProvider.accessToken!,
|
||||
giteaOwner!,
|
||||
giteaRepository!,
|
||||
);
|
||||
|
||||
writeStream.write(`\nCloning Repo ${repoClone} to ${outputPath}...\n`);
|
||||
|
||||
@@ -269,9 +290,12 @@ export const cloneRawGiteaRepository = async (entity: Compose) => {
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
|
||||
const repoClone = `${giteaOwner}/${giteaRepository}.git`;
|
||||
const baseUrl = giteaProvider.giteaUrl.replace(/^https?:\/\//, "");
|
||||
const cloneUrl = `https://oauth2:${giteaProvider.accessToken}@${baseUrl}/${repoClone}`;
|
||||
const cloneUrl = buildGiteaCloneUrl(
|
||||
giteaProvider.giteaUrl,
|
||||
giteaProvider.accessToken!,
|
||||
giteaOwner!,
|
||||
giteaRepository!,
|
||||
);
|
||||
|
||||
try {
|
||||
await spawnAsync("git", [
|
||||
@@ -317,9 +341,13 @@ export const cloneRawGiteaRepositoryRemote = async (compose: Compose) => {
|
||||
const giteaProvider = await findGiteaById(giteaId);
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
const repoClone = `${giteaOwner}/${giteaRepository}.git`;
|
||||
const baseUrl = giteaProvider.giteaUrl.replace(/^https?:\/\//, "");
|
||||
const cloneUrl = `https://oauth2:${giteaProvider.accessToken}@${baseUrl}/${repoClone}`;
|
||||
const cloneUrl = buildGiteaCloneUrl(
|
||||
giteaProvider.giteaUrl,
|
||||
giteaProvider.accessToken!,
|
||||
giteaOwner!,
|
||||
giteaRepository!,
|
||||
);
|
||||
|
||||
try {
|
||||
const command = `
|
||||
rm -rf ${outputPath};
|
||||
|
||||
21
packages/server/src/utils/startup/cancell-deployments.ts
Normal file
21
packages/server/src/utils/startup/cancell-deployments.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { deployments } from "@dokploy/server/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "../../db/index";
|
||||
|
||||
export const initCancelDeployments = async () => {
|
||||
try {
|
||||
console.log("Setting up cancel deployments....");
|
||||
|
||||
const result = await db
|
||||
.update(deployments)
|
||||
.set({
|
||||
status: "cancelled",
|
||||
})
|
||||
.where(eq(deployments.status, "running"))
|
||||
.returning();
|
||||
|
||||
console.log(`Cancelled ${result.length} deployments`);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user