Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions apps/dokploy/__test__/server/mechanizeDockerContainer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const { inspectMock, getServiceMock, createServiceMock, getRemoteDockerMock } =
const inspect = vi.fn<() => Promise<never>>();
const getService = vi.fn(() => ({ inspect }));
const createService = vi.fn<
(opts: MockCreateServiceOptions) => Promise<void>
(authconfig: unknown, opts: MockCreateServiceOptions) => Promise<void>
>(async () => undefined);
const getRemoteDocker = vi.fn(async () => ({
getService,
Expand Down Expand Up @@ -83,12 +83,12 @@ describe("mechanizeDockerContainer", () => {

expect(createServiceMock).toHaveBeenCalledTimes(1);
const call = createServiceMock.mock.calls[0] as
| [MockCreateServiceOptions]
| [unknown, MockCreateServiceOptions]
| undefined;
if (!call) {
throw new Error("createServiceMock should have been called once");
}
const [settings] = call;
const [, settings] = call;
expect(settings.TaskTemplate?.ContainerSpec?.StopGracePeriod).toBe(0);
expect(typeof settings.TaskTemplate?.ContainerSpec?.StopGracePeriod).toBe(
"number",
Expand All @@ -102,12 +102,12 @@ describe("mechanizeDockerContainer", () => {

expect(createServiceMock).toHaveBeenCalledTimes(1);
const call = createServiceMock.mock.calls[0] as
| [MockCreateServiceOptions]
| [unknown, MockCreateServiceOptions]
| undefined;
if (!call) {
throw new Error("createServiceMock should have been called once");
}
const [settings] = call;
const [, settings] = call;
expect(settings.TaskTemplate?.ContainerSpec).not.toHaveProperty(
"StopGracePeriod",
);
Expand All @@ -127,7 +127,7 @@ describe("mechanizeDockerContainer", () => {
if (!call) {
throw new Error("createServiceMock should have been called once");
}
const [settings] = call;
const [, settings] = call;
expect(settings.TaskTemplate?.ContainerSpec?.Ulimits).toEqual(ulimits);
});

Expand All @@ -141,7 +141,7 @@ describe("mechanizeDockerContainer", () => {
if (!call) {
throw new Error("createServiceMock should have been called once");
}
const [settings] = call;
const [, settings] = call;
expect(settings.TaskTemplate?.ContainerSpec).not.toHaveProperty("Ulimits");
});

Expand All @@ -155,7 +155,7 @@ describe("mechanizeDockerContainer", () => {
if (!call) {
throw new Error("createServiceMock should have been called once");
}
const [settings] = call;
const [, settings] = call;
expect(settings.TaskTemplate?.ContainerSpec).not.toHaveProperty("Ulimits");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ const schema = z
previewCertificateType: z.enum(["letsencrypt", "none", "custom"]),
previewCustomCertResolver: z.string().optional(),
previewRequireCollaboratorPermissions: z.boolean(),
previewDockerImage: z.string().optional(),
previewRegistryId: z.string().optional(),
})
.superRefine((input, ctx) => {
if (
Expand Down Expand Up @@ -84,6 +86,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
api.application.update.useMutation();

const { data, refetch } = api.application.one.useQuery({ applicationId });
const { data: registries } = api.registry.all.useQuery();

const form = useForm<Schema>({
defaultValues: {
Expand All @@ -96,6 +99,8 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
previewPath: "/",
previewCertificateType: "none",
previewRequireCollaboratorPermissions: true,
previewDockerImage: "",
previewRegistryId: "",
},
resolver: zodResolver(schema),
});
Expand Down Expand Up @@ -124,6 +129,8 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
previewCustomCertResolver: data.previewCustomCertResolver || "",
previewRequireCollaboratorPermissions:
data.previewRequireCollaboratorPermissions ?? true,
previewDockerImage: data.previewDockerImage || "",
previewRegistryId: data.previewRegistryId || "",
});
}
}, [data]);
Expand All @@ -144,6 +151,11 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
previewCustomCertResolver: formData.previewCustomCertResolver,
previewRequireCollaboratorPermissions:
formData.previewRequireCollaboratorPermissions,
previewDockerImage: formData.previewDockerImage || null,
previewRegistryId:
formData.previewRegistryId === "none" || !formData.previewRegistryId
? null
: formData.previewRegistryId,
})
.then(() => {
toast.success("Preview Deployments settings updated");
Expand Down Expand Up @@ -457,6 +469,99 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
/>
</div>

<div className="grid gap-4 lg:grid-cols-2">
<FormField
control={form.control}
name="previewDockerImage"
render={({ field }) => (
<FormItem className="md:col-span-2">
<div className="flex items-center gap-2">
<FormLabel>Docker Image</FormLabel>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
</TooltipTrigger>
<TooltipContent className="max-w-sm">
<p>
Skip building from source and pull a pre-built
image instead. Supports template variables:
</p>
<ul className="mt-1 text-xs">
<li>
<code>{"{owner}"}</code> - GitHub org/user
</li>
<li>
<code>{"{repository}"}</code> - Repo name
</li>
<li>
<code>{"{branch}"}</code> - PR branch
</li>
<li>
<code>{"{pr_number}"}</code> - PR number
</li>
<li>
<code>{"{app_name}"}</code> - App name
</li>
</ul>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input
placeholder="ghcr.io/{owner}/{repository}:pr-{pr_number}"
{...field}
/>
</FormControl>
<FormDescription>
Leave empty to build from source (default behavior).
</FormDescription>
<FormMessage />
</FormItem>
)}
/>

{form.watch("previewDockerImage") && (
<FormField
control={form.control}
name="previewRegistryId"
render={({ field }) => (
<FormItem className="md:col-span-2">
<FormLabel>Image Registry</FormLabel>
<Select
onValueChange={field.onChange}
value={field.value || "none"}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a registry for authentication" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="none">
None (public image)
</SelectItem>
{registries?.map((reg) => (
<SelectItem
key={reg.registryId}
value={reg.registryId}
>
{reg.registryName} ({reg.registryUrl})
</SelectItem>
))}
</SelectContent>
</Select>
<FormDescription>
Registry credentials for pulling the image.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
</div>

<FormField
control={form.control}
name="env"
Expand Down
3 changes: 3 additions & 0 deletions apps/dokploy/drizzle/0148_noisy_sharon_carter.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ALTER TABLE "application" ADD COLUMN "previewDockerImage" text;--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "previewRegistryId" text;--> statement-breakpoint
ALTER TABLE "application" ADD CONSTRAINT "application_previewRegistryId_registry_registryId_fk" FOREIGN KEY ("previewRegistryId") REFERENCES "public"."registry"("registryId") ON DELETE set null ON UPDATE no action;
Loading