mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat: add deployable option to randomize and prevent colission in duplicate templates
This commit is contained in:
parent
97b77e526d
commit
6f2148c060
@ -15,6 +15,7 @@ import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { validateAndFormatYAML } from "../../application/advanced/traefik/update-traefik-config";
|
||||
import { RandomizeCompose } from "./randomize-compose";
|
||||
import { RandomizeDeployable } from "./randomize-deployable";
|
||||
|
||||
interface Props {
|
||||
composeId: string;
|
||||
@ -126,6 +127,7 @@ services:
|
||||
<div className="flex justify-between flex-col lg:flex-row gap-2">
|
||||
<div className="w-full flex flex-col lg:flex-row gap-4 items-end">
|
||||
<RandomizeCompose composeId={composeId} />
|
||||
<RandomizeDeployable composeId={composeId} />
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
|
@ -0,0 +1,196 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle, Dices } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
interface Props {
|
||||
composeId: string;
|
||||
}
|
||||
|
||||
const schema = z.object({
|
||||
deployable: z.boolean().optional(),
|
||||
});
|
||||
|
||||
type Schema = z.infer<typeof schema>;
|
||||
|
||||
export const RandomizeDeployable = ({ composeId }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const [compose, setCompose] = useState<string>("");
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { mutateAsync, error, isError } =
|
||||
api.compose.randomizeDeployableCompose.useMutation();
|
||||
|
||||
const { mutateAsync: updateCompose } = api.compose.update.useMutation();
|
||||
|
||||
const { data, refetch } = api.compose.one.useQuery(
|
||||
{ composeId },
|
||||
{ enabled: !!composeId },
|
||||
);
|
||||
|
||||
const form = useForm<Schema>({
|
||||
defaultValues: {
|
||||
deployable: false,
|
||||
},
|
||||
resolver: zodResolver(schema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
deployable: data?.deployable || false,
|
||||
});
|
||||
}
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
|
||||
|
||||
const onSubmit = async (formData: Schema) => {
|
||||
await updateCompose({
|
||||
composeId,
|
||||
deployable: formData?.deployable || false,
|
||||
})
|
||||
.then(async (data) => {
|
||||
randomizeCompose();
|
||||
refetch();
|
||||
toast.success("Compose updated");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error randomizing the compose");
|
||||
});
|
||||
};
|
||||
|
||||
const randomizeCompose = async () => {
|
||||
await mutateAsync({
|
||||
composeId,
|
||||
suffix: data?.appName || "",
|
||||
})
|
||||
.then(async (data) => {
|
||||
await utils.project.all.invalidate();
|
||||
setCompose(data);
|
||||
toast.success("Compose randomized");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error randomizing the compose");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild onClick={() => randomizeCompose()}>
|
||||
<Button className="max-lg:w-full" variant="outline">
|
||||
<Dices className="h-4 w-4" />
|
||||
Randomize Deployable
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-6xl max-h-[50rem] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Randomize Deployable (Experimental)</DialogTitle>
|
||||
<DialogDescription>
|
||||
Use this in case you want to deploy the same compose file twice.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="text-sm text-muted-foreground flex flex-col gap-2">
|
||||
<span>
|
||||
This will randomize the compose file and will add a suffix to the
|
||||
property to avoid conflicts
|
||||
</span>
|
||||
<ul className="list-disc list-inside">
|
||||
<li>volumes</li>
|
||||
<li>networks</li>
|
||||
<li>services</li>
|
||||
</ul>
|
||||
<AlertBlock type="info">
|
||||
When you activate this option, we will include a env
|
||||
`DOKPLOY_SUFFIX` variable to the compose file so you can use it in
|
||||
your compose file, also we don't include the{" "}
|
||||
<code>dokploy-network</code> to any of the services by default.
|
||||
</AlertBlock>
|
||||
</div>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
id="hook-form-add-project"
|
||||
className="grid w-full gap-4"
|
||||
>
|
||||
{isError && (
|
||||
<div className="flex flex-row gap-4 rounded-lg items-center bg-red-50 p-2 dark:bg-red-950">
|
||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
||||
<span className="text-sm text-red-600 dark:text-red-400">
|
||||
{error?.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col lg:flex-col gap-4 w-full ">
|
||||
<div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="deployable"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Apply Randomize ({data?.appName})</FormLabel>
|
||||
<FormDescription>
|
||||
Apply randomize to the compose file.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col lg:flex-row gap-4 w-full items-end justify-end">
|
||||
<Button
|
||||
form="hook-form-add-project"
|
||||
type="submit"
|
||||
className="lg:w-fit"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<pre>
|
||||
<CodeEditor
|
||||
value={compose || ""}
|
||||
language="yaml"
|
||||
readOnly
|
||||
height="50rem"
|
||||
/>
|
||||
</pre>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
1
apps/dokploy/drizzle/0064_previous_agent_brand.sql
Normal file
1
apps/dokploy/drizzle/0064_previous_agent_brand.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER TABLE "compose" ADD COLUMN "deployable" boolean DEFAULT false NOT NULL;
|
4485
apps/dokploy/drizzle/meta/0064_snapshot.json
Normal file
4485
apps/dokploy/drizzle/meta/0064_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -449,6 +449,13 @@
|
||||
"when": 1738522845992,
|
||||
"tag": "0063_panoramic_dreadnoughts",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 64,
|
||||
"version": "7",
|
||||
"when": 1738564387043,
|
||||
"tag": "0064_previous_agent_brand",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
@ -46,6 +46,7 @@ import {
|
||||
findServerById,
|
||||
loadServices,
|
||||
randomizeComposeFile,
|
||||
randomizeDeployableComposeFile,
|
||||
removeCompose,
|
||||
removeComposeDirectory,
|
||||
removeDeploymentsByComposeId,
|
||||
@ -216,6 +217,21 @@ export const composeRouter = createTRPCRouter({
|
||||
}
|
||||
return await randomizeComposeFile(input.composeId, input.suffix);
|
||||
}),
|
||||
randomizeDeployableCompose: protectedProcedure
|
||||
.input(apiRandomizeCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (compose.project.adminId !== ctx.user.adminId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to randomize this compose",
|
||||
});
|
||||
}
|
||||
return await randomizeDeployableComposeFile(
|
||||
input.composeId,
|
||||
input.suffix,
|
||||
);
|
||||
}),
|
||||
getConvertedCompose: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.query(async ({ input, ctx }) => {
|
||||
|
@ -69,6 +69,7 @@ export const compose = pgTable("compose", {
|
||||
composePath: text("composePath").notNull().default("./docker-compose.yml"),
|
||||
suffix: text("suffix").notNull().default(""),
|
||||
randomize: boolean("randomize").notNull().default(false),
|
||||
deployable: boolean("deployable").notNull().default(false),
|
||||
composeStatus: applicationStatus("composeStatus").notNull().default("idle"),
|
||||
projectId: text("projectId")
|
||||
.notNull()
|
||||
|
@ -73,6 +73,7 @@ export * from "./utils/builders/utils";
|
||||
export * from "./utils/cluster/upload";
|
||||
|
||||
export * from "./utils/docker/compose";
|
||||
export * from "./utils/docker/collision";
|
||||
export * from "./utils/docker/domain";
|
||||
export * from "./utils/docker/utils";
|
||||
export * from "./utils/docker/types";
|
||||
|
@ -34,6 +34,9 @@ export const buildCompose = async (compose: ComposeNested, logPath: string) => {
|
||||
await writeDomainsToCompose(compose, domains);
|
||||
createEnvFile(compose);
|
||||
|
||||
await execAsync(
|
||||
`docker network inspect ${compose.appName} >/dev/null 2>&1 || docker network create --attachable ${compose.appName}`,
|
||||
);
|
||||
const logContent = `
|
||||
App Name: ${appName}
|
||||
Build Compose 🐳
|
||||
@ -73,6 +76,10 @@ export const buildCompose = async (compose: ComposeNested, logPath: string) => {
|
||||
},
|
||||
);
|
||||
|
||||
await execAsync(
|
||||
`docker network connect tes-umami-e842bc $(docker ps --filter "name=dokploy-traefik" -q) >/dev/null 2>&1`,
|
||||
);
|
||||
|
||||
writeStream.write("Docker Compose Deployed: ✅");
|
||||
} catch (error) {
|
||||
writeStream.write(`Error ❌ ${(error as Error).message}`);
|
||||
|
51
packages/server/src/utils/docker/collision.ts
Normal file
51
packages/server/src/utils/docker/collision.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { findComposeById } from "@dokploy/server/services/compose";
|
||||
import { addAppNameToAllContainerNames } from "./collision/container-name";
|
||||
import { addAppNameToAllServiceNames } from "./collision/root-network";
|
||||
import { addSuffixToAllVolumes } from "./compose/volume";
|
||||
import type { ComposeSpecification } from "./types";
|
||||
import { dump, load } from "js-yaml";
|
||||
import { generateRandomHash } from "./compose";
|
||||
|
||||
export const addAppNameToPreventCollision = (
|
||||
composeData: ComposeSpecification,
|
||||
appName: string,
|
||||
): ComposeSpecification => {
|
||||
let updatedComposeData = { ...composeData };
|
||||
|
||||
updatedComposeData = addAppNameToAllContainerNames(
|
||||
updatedComposeData,
|
||||
appName,
|
||||
);
|
||||
updatedComposeData = addAppNameToAllServiceNames(updatedComposeData, appName);
|
||||
updatedComposeData = addSuffixToAllVolumes(updatedComposeData, appName);
|
||||
return updatedComposeData;
|
||||
};
|
||||
|
||||
export const randomizeDeployableComposeFile = async (
|
||||
composeId: string,
|
||||
suffix?: string,
|
||||
) => {
|
||||
const compose = await findComposeById(composeId);
|
||||
const composeFile = compose.composeFile;
|
||||
const composeData = load(composeFile) as ComposeSpecification;
|
||||
|
||||
const randomSuffix = suffix || compose.appName || generateRandomHash();
|
||||
|
||||
const newComposeFile = addAppNameToPreventCollision(
|
||||
composeData,
|
||||
randomSuffix,
|
||||
);
|
||||
|
||||
return dump(newComposeFile);
|
||||
};
|
||||
|
||||
export const randomizeDeployableSpecificationFile = (
|
||||
composeSpec: ComposeSpecification,
|
||||
suffix?: string,
|
||||
) => {
|
||||
if (!suffix) {
|
||||
return composeSpec;
|
||||
}
|
||||
const newComposeFile = addAppNameToPreventCollision(composeSpec, suffix);
|
||||
return newComposeFile;
|
||||
};
|
26
packages/server/src/utils/docker/collision/container-name.ts
Normal file
26
packages/server/src/utils/docker/collision/container-name.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import type { ComposeSpecification, DefinitionsService } from "../types";
|
||||
import _ from "lodash";
|
||||
|
||||
export const addAppNameToContainerNames = (
|
||||
services: { [key: string]: DefinitionsService },
|
||||
appName: string,
|
||||
): { [key: string]: DefinitionsService } => {
|
||||
return _.mapValues(services, (service, serviceName) => {
|
||||
service.container_name = `${appName}-${serviceName}`;
|
||||
return service;
|
||||
});
|
||||
};
|
||||
|
||||
export const addAppNameToAllContainerNames = (
|
||||
composeData: ComposeSpecification,
|
||||
appName: string,
|
||||
): ComposeSpecification => {
|
||||
const updatedComposeData = { ...composeData };
|
||||
if (updatedComposeData.services) {
|
||||
updatedComposeData.services = addAppNameToContainerNames(
|
||||
updatedComposeData.services,
|
||||
appName,
|
||||
);
|
||||
}
|
||||
return updatedComposeData;
|
||||
};
|
62
packages/server/src/utils/docker/collision/root-network.ts
Normal file
62
packages/server/src/utils/docker/collision/root-network.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import type { ComposeSpecification, DefinitionsService } from "../types";
|
||||
import _ from "lodash";
|
||||
|
||||
export const addAppNameToRootNetwork = (
|
||||
composeData: ComposeSpecification,
|
||||
appName: string,
|
||||
): ComposeSpecification => {
|
||||
const updatedComposeData = { ...composeData };
|
||||
|
||||
// Initialize networks if it doesn't exist
|
||||
if (!updatedComposeData.networks) {
|
||||
updatedComposeData.networks = {};
|
||||
}
|
||||
|
||||
// Add the new network with the app name
|
||||
updatedComposeData.networks[appName] = {
|
||||
name: appName,
|
||||
external: true,
|
||||
};
|
||||
|
||||
return updatedComposeData;
|
||||
};
|
||||
|
||||
export const addAppNameToServiceNetworks = (
|
||||
services: { [key: string]: DefinitionsService },
|
||||
appName: string,
|
||||
): { [key: string]: DefinitionsService } => {
|
||||
return _.mapValues(services, (service) => {
|
||||
if (!service.networks) {
|
||||
service.networks = [appName];
|
||||
return service;
|
||||
}
|
||||
|
||||
if (Array.isArray(service.networks)) {
|
||||
if (!service.networks.includes(appName)) {
|
||||
service.networks.push(appName);
|
||||
}
|
||||
} else {
|
||||
service.networks[appName] = {};
|
||||
}
|
||||
|
||||
return service;
|
||||
});
|
||||
};
|
||||
|
||||
export const addAppNameToAllServiceNames = (
|
||||
composeData: ComposeSpecification,
|
||||
appName: string,
|
||||
): ComposeSpecification => {
|
||||
let updatedComposeData = { ...composeData };
|
||||
|
||||
updatedComposeData = addAppNameToRootNetwork(updatedComposeData, appName);
|
||||
|
||||
if (updatedComposeData.services) {
|
||||
updatedComposeData.services = addAppNameToServiceNetworks(
|
||||
updatedComposeData.services,
|
||||
appName,
|
||||
);
|
||||
}
|
||||
|
||||
return updatedComposeData;
|
||||
};
|
@ -33,6 +33,7 @@ import type {
|
||||
PropertiesNetworks,
|
||||
} from "./types";
|
||||
import { encodeBase64 } from "./utils";
|
||||
import { randomizeDeployableSpecificationFile } from "./collision";
|
||||
|
||||
export const cloneCompose = async (compose: Compose) => {
|
||||
if (compose.sourceType === "github") {
|
||||
@ -190,7 +191,13 @@ export const addDomainToCompose = async (
|
||||
return null;
|
||||
}
|
||||
|
||||
if (compose.randomize) {
|
||||
if (compose.deployable) {
|
||||
const randomized = randomizeDeployableSpecificationFile(
|
||||
result,
|
||||
compose.suffix || compose.appName,
|
||||
);
|
||||
result = randomized;
|
||||
} else if (compose.randomize) {
|
||||
const randomized = randomizeSpecificationFile(result, compose.suffix);
|
||||
result = randomized;
|
||||
}
|
||||
@ -240,14 +247,18 @@ export const addDomainToCompose = async (
|
||||
labels.push(...httpLabels);
|
||||
}
|
||||
|
||||
// Add the dokploy-network to the service
|
||||
result.services[serviceName].networks = addDokployNetworkToService(
|
||||
result.services[serviceName].networks,
|
||||
);
|
||||
if (!compose.deployable) {
|
||||
// Add the dokploy-network to the service
|
||||
result.services[serviceName].networks = addDokployNetworkToService(
|
||||
result.services[serviceName].networks,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Add dokploy-network to the root of the compose file
|
||||
result.networks = addDokployNetworkToRoot(result.networks);
|
||||
if (!compose.deployable) {
|
||||
result.networks = addDokployNetworkToRoot(result.networks);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user