mirror of
https://github.com/andwati/dokploy.git
synced 2026-11-09 05:25:18 +00:00
Merge pull request #2232 from perinm/feature/stop-grace-period-2227
feat: Add stop_grace_period to swarm settings
This commit is contained in:
@@ -133,6 +133,7 @@ const baseApp: ApplicationNested = {
|
||||
username: null,
|
||||
dockerContextPath: null,
|
||||
rollbackActive: false,
|
||||
stopGracePeriodSwarm: null,
|
||||
};
|
||||
|
||||
describe("unzipDrop using real zip files", () => {
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { ApplicationNested } from "@dokploy/server/utils/builders";
|
||||
import { mechanizeDockerContainer } from "@dokploy/server/utils/builders";
|
||||
|
||||
type MockCreateServiceOptions = {
|
||||
StopGracePeriod?: number;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
const { inspectMock, getServiceMock, createServiceMock, getRemoteDockerMock } =
|
||||
vi.hoisted(() => {
|
||||
const inspect = vi.fn<[], Promise<never>>();
|
||||
const getService = vi.fn(() => ({ inspect }));
|
||||
const createService = vi.fn<[MockCreateServiceOptions], Promise<void>>(
|
||||
async () => undefined,
|
||||
);
|
||||
const getRemoteDocker = vi.fn(async () => ({
|
||||
getService,
|
||||
createService,
|
||||
}));
|
||||
return {
|
||||
inspectMock: inspect,
|
||||
getServiceMock: getService,
|
||||
createServiceMock: createService,
|
||||
getRemoteDockerMock: getRemoteDocker,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@dokploy/server/utils/servers/remote-docker", () => ({
|
||||
getRemoteDocker: getRemoteDockerMock,
|
||||
}));
|
||||
|
||||
const createApplication = (
|
||||
overrides: Partial<ApplicationNested> = {},
|
||||
): ApplicationNested =>
|
||||
({
|
||||
appName: "test-app",
|
||||
buildType: "dockerfile",
|
||||
env: null,
|
||||
mounts: [],
|
||||
cpuLimit: null,
|
||||
memoryLimit: null,
|
||||
memoryReservation: null,
|
||||
cpuReservation: null,
|
||||
command: null,
|
||||
ports: [],
|
||||
sourceType: "docker",
|
||||
dockerImage: "example:latest",
|
||||
registry: null,
|
||||
environment: {
|
||||
project: { env: null },
|
||||
env: null,
|
||||
},
|
||||
replicas: 1,
|
||||
stopGracePeriodSwarm: 0n,
|
||||
serverId: "server-id",
|
||||
...overrides,
|
||||
}) as unknown as ApplicationNested;
|
||||
|
||||
describe("mechanizeDockerContainer", () => {
|
||||
beforeEach(() => {
|
||||
inspectMock.mockReset();
|
||||
inspectMock.mockRejectedValue(new Error("service not found"));
|
||||
getServiceMock.mockClear();
|
||||
createServiceMock.mockClear();
|
||||
getRemoteDockerMock.mockClear();
|
||||
getRemoteDockerMock.mockResolvedValue({
|
||||
getService: getServiceMock,
|
||||
createService: createServiceMock,
|
||||
});
|
||||
});
|
||||
|
||||
it("converts bigint stopGracePeriodSwarm to a number and keeps zero values", async () => {
|
||||
const application = createApplication({ stopGracePeriodSwarm: 0n });
|
||||
|
||||
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.StopGracePeriod).toBe(0);
|
||||
expect(typeof settings.StopGracePeriod).toBe("number");
|
||||
});
|
||||
|
||||
it("omits StopGracePeriod when stopGracePeriodSwarm is null", async () => {
|
||||
const application = createApplication({ stopGracePeriodSwarm: 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).not.toHaveProperty("StopGracePeriod");
|
||||
});
|
||||
});
|
||||
@@ -111,6 +111,7 @@ const baseApp: ApplicationNested = {
|
||||
updateConfigSwarm: null,
|
||||
username: null,
|
||||
dockerContextPath: null,
|
||||
stopGracePeriodSwarm: null,
|
||||
};
|
||||
|
||||
const baseDomain: Domain = {
|
||||
|
||||
+72
-1
@@ -25,6 +25,7 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -176,10 +177,18 @@ const addSwarmSettings = z.object({
|
||||
modeSwarm: createStringToJSONSchema(ServiceModeSwarmSchema).nullable(),
|
||||
labelsSwarm: createStringToJSONSchema(LabelsSwarmSchema).nullable(),
|
||||
networkSwarm: createStringToJSONSchema(NetworkSwarmSchema).nullable(),
|
||||
stopGracePeriodSwarm: z.bigint().nullable(),
|
||||
});
|
||||
|
||||
type AddSwarmSettings = z.infer<typeof addSwarmSettings>;
|
||||
|
||||
const hasStopGracePeriodSwarm = (
|
||||
value: unknown,
|
||||
): value is { stopGracePeriodSwarm: bigint | number | string | null } =>
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
"stopGracePeriodSwarm" in value;
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||
@@ -224,12 +233,22 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
|
||||
modeSwarm: null,
|
||||
labelsSwarm: null,
|
||||
networkSwarm: null,
|
||||
stopGracePeriodSwarm: null,
|
||||
},
|
||||
resolver: zodResolver(addSwarmSettings),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
const stopGracePeriodValue = hasStopGracePeriodSwarm(data)
|
||||
? data.stopGracePeriodSwarm
|
||||
: null;
|
||||
const normalizedStopGracePeriod =
|
||||
stopGracePeriodValue === null || stopGracePeriodValue === undefined
|
||||
? null
|
||||
: typeof stopGracePeriodValue === "bigint"
|
||||
? stopGracePeriodValue
|
||||
: BigInt(stopGracePeriodValue);
|
||||
form.reset({
|
||||
healthCheckSwarm: data.healthCheckSwarm
|
||||
? JSON.stringify(data.healthCheckSwarm, null, 2)
|
||||
@@ -255,6 +274,7 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
|
||||
networkSwarm: data.networkSwarm
|
||||
? JSON.stringify(data.networkSwarm, null, 2)
|
||||
: null,
|
||||
stopGracePeriodSwarm: normalizedStopGracePeriod,
|
||||
});
|
||||
}
|
||||
}, [form, form.reset, data]);
|
||||
@@ -275,6 +295,7 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
|
||||
modeSwarm: data.modeSwarm,
|
||||
labelsSwarm: data.labelsSwarm,
|
||||
networkSwarm: data.networkSwarm,
|
||||
stopGracePeriodSwarm: data.stopGracePeriodSwarm ?? null,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Swarm settings updated");
|
||||
@@ -774,7 +795,57 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="stopGracePeriodSwarm"
|
||||
render={({ field }) => (
|
||||
<FormItem className="relative max-lg:px-4 lg:pl-6 ">
|
||||
<FormLabel>Stop Grace Period (nanoseconds)</FormLabel>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<FormDescription className="break-all w-fit flex flex-row gap-1 items-center">
|
||||
Duration in nanoseconds
|
||||
<HelpCircle className="size-4 text-muted-foreground" />
|
||||
</FormDescription>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="w-full z-[999]"
|
||||
align="start"
|
||||
side="bottom"
|
||||
>
|
||||
<code>
|
||||
<pre>
|
||||
{`Enter duration in nanoseconds:
|
||||
• 30000000000 - 30 seconds
|
||||
• 120000000000 - 2 minutes
|
||||
• 3600000000000 - 1 hour
|
||||
• 0 - no grace period`}
|
||||
</pre>
|
||||
</code>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="30000000000"
|
||||
className="font-mono"
|
||||
{...field}
|
||||
value={field?.value?.toString() || ""}
|
||||
onChange={(e) =>
|
||||
field.onChange(
|
||||
e.target.value ? BigInt(e.target.value) : null,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<pre>
|
||||
<FormMessage />
|
||||
</pre>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<DialogFooter className="flex w-full flex-row justify-end md:col-span-2 m-0 sticky bottom-0 right-0 bg-muted border">
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE "application" ADD COLUMN "stopGracePeriodSwarm" bigint;--> statement-breakpoint
|
||||
ALTER TABLE "mariadb" ADD COLUMN "stopGracePeriodSwarm" bigint;--> statement-breakpoint
|
||||
ALTER TABLE "mongo" ADD COLUMN "stopGracePeriodSwarm" bigint;--> statement-breakpoint
|
||||
ALTER TABLE "mysql" ADD COLUMN "stopGracePeriodSwarm" bigint;--> statement-breakpoint
|
||||
ALTER TABLE "postgres" ADD COLUMN "stopGracePeriodSwarm" bigint;--> statement-breakpoint
|
||||
ALTER TABLE "redis" ADD COLUMN "stopGracePeriodSwarm" bigint;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -799,6 +799,13 @@
|
||||
"when": 1758960816504,
|
||||
"tag": "0113_complete_rafael_vega",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 114,
|
||||
"version": "7",
|
||||
"when": 1759643172958,
|
||||
"tag": "0114_dry_black_tom",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user