Merge pull request #174 from Dokploy/canary

v0.2.5
This commit is contained in:
Mauricio Siu
2024-06-29 13:29:39 -06:00
committed by GitHub
33 changed files with 622 additions and 178 deletions

View File

@@ -26,7 +26,7 @@ Dokploy include multiples features to make your life easier.
* **Traefik Integration**: Automatically integrates with Traefik for routing and load balancing. * **Traefik Integration**: Automatically integrates with Traefik for routing and load balancing.
* **Real-time Monitoring**: Monitor CPU, memory, storage, and network usage, for every resource. * **Real-time Monitoring**: Monitor CPU, memory, storage, and network usage, for every resource.
* **Docker Management**: Easily deploy and manage Docker containers. * **Docker Management**: Easily deploy and manage Docker containers.
* **CLI (Soon⌛)**: Manage your applications and databases using the command line. * **CLI/API**: Manage your applications and databases using the command line or trought the API.
* **Self-Hosted**: Self-host Dokploy on your VPS. * **Self-Hosted**: Self-host Dokploy on your VPS.

View File

@@ -86,6 +86,11 @@ export const ShowDeployments = ({ applicationId }: Props) => {
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
{deployment.title} {deployment.title}
</span> </span>
{deployment.description && (
<span className="text-sm text-muted-foreground">
{deployment.description}
</span>
)}
</div> </div>
<div className="flex flex-col items-end gap-2"> <div className="flex flex-col items-end gap-2">
<div className="text-sm capitalize text-muted-foreground"> <div className="text-sm capitalize text-muted-foreground">

View File

@@ -0,0 +1,79 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { RefreshCcw } from "lucide-react";
import { GenerateTraefikMe } from "./generate-traefikme";
import { GenerateWildCard } from "./generate-wildcard";
import Link from "next/link";
import { api } from "@/utils/api";
interface Props {
applicationId: string;
}
export const GenerateDomain = ({ applicationId }: Props) => {
return (
<Dialog>
<DialogTrigger className="" asChild>
<Button variant="secondary">
Generate Domain
<RefreshCcw className="size-4 text-muted-foreground " />
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
<DialogHeader>
<DialogTitle>Generate Domain</DialogTitle>
<DialogDescription>
Generate Domains for your applications
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-4 w-full">
<ul className="flex flex-col gap-4">
<li className="flex flex-row items-center gap-4">
<div className="flex flex-col gap-2">
<div className="text-base font-bold">
1. Generate TraefikMe Domain
</div>
<div className="text-sm text-muted-foreground">
This option generates a free domain provided by{" "}
<Link
href="https://traefik.me"
className="text-primary"
target="_blank"
>
TraefikMe
</Link>
. We recommend using this for quick domain testing or if you
don't have a domain yet.
</div>
</div>
</li>
{/* <li className="flex flex-row items-center gap-4">
<div className="flex flex-col gap-2">
<div className="text-base font-bold">
2. Use Wildcard Domain
</div>
<div className="text-sm text-muted-foreground">
To use this option, you need to set up an 'A' record in your
domain provider. For example, create a record for
*.yourdomain.com.
</div>
</div>
</li> */}
</ul>
<div className="flex flex-row gap-4 w-full">
<GenerateTraefikMe applicationId={applicationId} />
{/* <GenerateWildCard applicationId={applicationId} /> */}
</div>
</div>
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,69 @@
import React from "react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { RefreshCcw } from "lucide-react";
import { toast } from "sonner";
interface Props {
applicationId: string;
}
export const GenerateTraefikMe = ({ applicationId }: Props) => {
const { mutateAsync, isLoading } = api.domain.generateDomain.useMutation();
const utils = api.useUtils();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="secondary" isLoading={isLoading}>
Generate Domain
<RefreshCcw className="size-4 text-muted-foreground " />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Are you sure to generate a new domain?
</AlertDialogTitle>
<AlertDialogDescription>
This will generate a new domain and will be used to access to the
application
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync({
applicationId,
})
.then((data) => {
utils.domain.byApplicationId.invalidate({
applicationId: applicationId,
});
utils.application.readTraefikConfig.invalidate({
applicationId: applicationId,
});
toast.success("Generated Domain succesfully");
})
.catch(() => {
toast.error("Error to generate Domain");
});
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};

View File

@@ -0,0 +1,69 @@
import React from "react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { SquareAsterisk } from "lucide-react";
import { toast } from "sonner";
interface Props {
applicationId: string;
}
export const GenerateWildCard = ({ applicationId }: Props) => {
const { mutateAsync, isLoading } = api.domain.generateWildcard.useMutation();
const utils = api.useUtils();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="secondary" isLoading={isLoading}>
Generate Wildcard Domain
<SquareAsterisk className="size-4 text-muted-foreground " />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Are you sure to generate a new wildcard domain?
</AlertDialogTitle>
<AlertDialogDescription>
This will generate a new domain and will be used to access to the
application
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync({
applicationId,
})
.then((data) => {
utils.domain.byApplicationId.invalidate({
applicationId: applicationId,
});
utils.application.readTraefikConfig.invalidate({
applicationId: applicationId,
});
toast.success("Generated Domain succesfully");
})
.catch((e) => {
toast.error(`Error to generate Domain: ${e.message}`);
});
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};

View File

@@ -6,7 +6,7 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { ExternalLink, GlobeIcon } from "lucide-react"; import { ExternalLink, GlobeIcon, RefreshCcw } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
@@ -14,6 +14,7 @@ import { DeleteDomain } from "./delete-domain";
import Link from "next/link"; import Link from "next/link";
import { AddDomain } from "./add-domain"; import { AddDomain } from "./add-domain";
import { UpdateDomain } from "./update-domain"; import { UpdateDomain } from "./update-domain";
import { GenerateDomain } from "./generate-domain";
interface Props { interface Props {
applicationId: string; applicationId: string;
@@ -31,7 +32,7 @@ export const ShowDomains = ({ applicationId }: Props) => {
return ( return (
<div className="flex w-full flex-col gap-5 "> <div className="flex w-full flex-col gap-5 ">
<Card className="bg-background"> <Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between"> <CardHeader className="flex flex-row items-center flex-wrap gap-4 justify-between">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<CardTitle className="text-xl">Domains</CardTitle> <CardTitle className="text-xl">Domains</CardTitle>
<CardDescription> <CardDescription>
@@ -39,11 +40,16 @@ export const ShowDomains = ({ applicationId }: Props) => {
</CardDescription> </CardDescription>
</div> </div>
{data && data?.length > 0 && ( <div className="flex flex-row gap-4 flex-wrap">
<AddDomain applicationId={applicationId}> {data && data?.length > 0 && (
<GlobeIcon className="size-4" /> Add Domain <AddDomain applicationId={applicationId}>
</AddDomain> <GlobeIcon className="size-4" /> Add Domain
)} </AddDomain>
)}
{data && data?.length > 0 && (
<GenerateDomain applicationId={applicationId} />
)}
</div>
</CardHeader> </CardHeader>
<CardContent className="flex w-full flex-row gap-4"> <CardContent className="flex w-full flex-row gap-4">
{data?.length === 0 ? ( {data?.length === 0 ? (
@@ -53,9 +59,13 @@ export const ShowDomains = ({ applicationId }: Props) => {
To access to the application is required to set at least 1 To access to the application is required to set at least 1
domain domain
</span> </span>
<AddDomain applicationId={applicationId}> <div className="flex flex-row gap-4 flex-wrap">
<GlobeIcon className="size-4" /> Add Domain <AddDomain applicationId={applicationId}>
</AddDomain> <GlobeIcon className="size-4" /> Add Domain
</AddDomain>
<GenerateDomain applicationId={applicationId} />
</div>
</div> </div>
) : ( ) : (
<div className="flex w-full flex-col gap-4"> <div className="flex w-full flex-col gap-4">

View File

@@ -141,7 +141,13 @@ export const DockerMonitoring = ({
network: data.network[data.network.length - 1] ?? currentData.network, network: data.network[data.network.length - 1] ?? currentData.network,
disk: data.disk[data.disk.length - 1] ?? currentData.disk, disk: data.disk[data.disk.length - 1] ?? currentData.disk,
}); });
setAcummulativeData(data); setAcummulativeData({
block: data?.block || [],
cpu: data?.cpu || [],
disk: data?.disk || [],
memory: data?.memory || [],
network: data?.network || [],
});
}, [data]); }, [data]);
useEffect(() => { useEffect(() => {

View File

@@ -76,8 +76,8 @@ export const ShowProjects = () => {
project?.compose.length; project?.compose.length;
return ( return (
<div key={project.projectId} className="w-full lg:max-w-md"> <div key={project.projectId} className="w-full lg:max-w-md">
<Card className="group relative w-full bg-transparent transition-colors hover:bg-card"> <Link href={`/dashboard/project/${project.projectId}`}>
<Link href={`/dashboard/project/${project.projectId}`}> <Card className="group relative w-full bg-transparent transition-colors hover:bg-card">
<Button <Button
className="absolute -right-3 -top-3 size-9 translate-y-1 rounded-full p-0 opacity-0 transition-all duration-200 group-hover:translate-y-0 group-hover:opacity-100" className="absolute -right-3 -top-3 size-9 translate-y-1 rounded-full p-0 opacity-0 transition-all duration-200 group-hover:translate-y-0 group-hover:opacity-100"
size="sm" size="sm"
@@ -85,113 +85,122 @@ export const ShowProjects = () => {
> >
<ExternalLinkIcon className="size-3.5" /> <ExternalLinkIcon className="size-3.5" />
</Button> </Button>
</Link> <CardHeader>
<CardHeader> <CardTitle className="flex items-center justify-between gap-2">
<CardTitle className="flex items-center justify-between gap-2"> <span className="flex flex-col gap-1.5">
<span className="flex flex-col gap-1.5"> <div className="flex items-center gap-2">
<div className="flex items-center gap-2"> <BookIcon className="size-4 text-muted-foreground" />
<BookIcon className="size-4 text-muted-foreground" /> <span className="text-base font-medium leading-none">
<Link {project.name}
className="text-base font-medium leading-none" </span>
href={`/dashboard/project/${project.projectId}`} </div>
>
{project.name}
</Link>
</div>
<span className="text-sm font-medium text-muted-foreground"> <span className="text-sm font-medium text-muted-foreground">
{project.description} {project.description}
</span>
</span> </span>
</span> <div className="flex self-start space-x-1">
<div className="flex self-start space-x-1"> <DropdownMenu>
<DropdownMenu> <DropdownMenuTrigger asChild>
<DropdownMenuTrigger asChild> <Button
<Button variant="ghost" size="icon" className="px-2"> variant="ghost"
<MoreHorizontalIcon className="size-5" /> size="icon"
</Button> className="px-2"
</DropdownMenuTrigger> >
<DropdownMenuContent className="w-[200px] space-y-2"> <MoreHorizontalIcon className="size-5" />
<DropdownMenuLabel className="font-normal"> </Button>
Actions </DropdownMenuTrigger>
</DropdownMenuLabel> <DropdownMenuContent className="w-[200px] space-y-2">
<DropdownMenuLabel className="font-normal">
Actions
</DropdownMenuLabel>
<UpdateProject projectId={project.projectId} /> <div onClick={(e) => e.stopPropagation()}>
<UpdateProject projectId={project.projectId} />
</div>
{(auth?.rol === "admin" || <div onClick={(e) => e.stopPropagation()}>
user?.canDeleteProjects) && ( {(auth?.rol === "admin" ||
<AlertDialog> user?.canDeleteProjects) && (
<AlertDialogTrigger className="w-full"> <AlertDialog>
<DropdownMenuItem <AlertDialogTrigger className="w-full">
className="w-full cursor-pointer space-x-3" <DropdownMenuItem
onSelect={(e) => e.preventDefault()} className="w-full cursor-pointer space-x-3"
> onSelect={(e) => e.preventDefault()}
<TrashIcon className="size-4" /> >
<span>Delete</span> <TrashIcon className="size-4" />
</DropdownMenuItem> <span>Delete</span>
</AlertDialogTrigger> </DropdownMenuItem>
<AlertDialogContent> </AlertDialogTrigger>
<AlertDialogHeader> <AlertDialogContent>
<AlertDialogTitle> <AlertDialogHeader>
Are you sure to delete this project? <AlertDialogTitle>
</AlertDialogTitle> Are you sure to delete this project?
{!emptyServices ? ( </AlertDialogTitle>
<div className="flex flex-row gap-4 rounded-lg bg-yellow-50 p-2 dark:bg-yellow-950"> {!emptyServices ? (
<AlertTriangle className="text-yellow-600 dark:text-yellow-400" /> <div className="flex flex-row gap-4 rounded-lg bg-yellow-50 p-2 dark:bg-yellow-950">
<span className="text-sm text-yellow-600 dark:text-yellow-400"> <AlertTriangle className="text-yellow-600 dark:text-yellow-400" />
You have active services, please delete <span className="text-sm text-yellow-600 dark:text-yellow-400">
them first You have active services, please
</span> delete them first
</div> </span>
) : ( </div>
<AlertDialogDescription> ) : (
This action cannot be undone <AlertDialogDescription>
</AlertDialogDescription> This action cannot be undone
)} </AlertDialogDescription>
</AlertDialogHeader> )}
<AlertDialogFooter> </AlertDialogHeader>
<AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogFooter>
<AlertDialogAction <AlertDialogCancel>
disabled={!emptyServices} Cancel
onClick={async () => { </AlertDialogCancel>
await mutateAsync({ <AlertDialogAction
projectId: project.projectId, disabled={!emptyServices}
}) onClick={async () => {
.then(() => { await mutateAsync({
toast.success( projectId: project.projectId,
"Project delete succesfully", })
); .then(() => {
}) toast.success(
.catch(() => { "Project delete succesfully",
toast.error( );
"Error to delete this project", })
); .catch(() => {
}) toast.error(
.finally(() => { "Error to delete this project",
utils.project.all.invalidate(); );
}); })
}} .finally(() => {
> utils.project.all.invalidate();
Delete });
</AlertDialogAction> }}
</AlertDialogFooter> >
</AlertDialogContent> Delete
</AlertDialog> </AlertDialogAction>
)} </AlertDialogFooter>
</DropdownMenuContent> </AlertDialogContent>
</DropdownMenu> </AlertDialog>
)}
</div>
</DropdownMenuContent>
</DropdownMenu>
</div>
</CardTitle>
</CardHeader>
<CardFooter className="pt-4">
<div className="space-y-1 text-sm flex flex-row justify-between max-sm:flex-wrap w-full gap-2 sm:gap-4">
<DateTooltip date={project.createdAt}>
Created
</DateTooltip>
<span>
{totalServices}{" "}
{totalServices === 1 ? "service" : "services"}
</span>
</div> </div>
</CardTitle> </CardFooter>
</CardHeader> </Card>
<CardFooter className="pt-4"> </Link>
<div className="space-y-1 text-sm flex flex-row justify-between max-sm:flex-wrap w-full gap-2 sm:gap-4">
<DateTooltip date={project.createdAt}>Created</DateTooltip>
<span>
{totalServices}{" "}
{totalServices === 1 ? "service" : "services"}
</span>
</div>
</CardFooter>
</Card>
</div> </div>
); );
})} })}

View File

@@ -0,0 +1 @@
ALTER TABLE "deployment" ADD COLUMN "description" text;

View File

@@ -1 +0,0 @@
ALTER TABLE "user" ALTER COLUMN "token" DROP NOT NULL;

View File

@@ -1,5 +1,5 @@
{ {
"id": "7610c85e-c3e4-4a32-8ce9-7f48b298f956", "id": "ec852f38-886a-43b4-9295-73984ed8ef45",
"prevId": "2d8d7670-b942-4573-9c44-6e81d2a2fa16", "prevId": "2d8d7670-b942-4573-9c44-6e81d2a2fa16",
"version": "6", "version": "6",
"dialect": "postgresql", "dialect": "postgresql",
@@ -465,7 +465,7 @@
"name": "token", "name": "token",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false "notNull": true
}, },
"isRegistered": { "isRegistered": {
"name": "isRegistered", "name": "isRegistered",
@@ -1585,6 +1585,12 @@
"primaryKey": false, "primaryKey": false,
"notNull": true "notNull": true
}, },
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"status": { "status": {
"name": "status", "name": "status",
"type": "deploymentStatus", "type": "deploymentStatus",

View File

@@ -124,8 +124,8 @@
{ {
"idx": 17, "idx": 17,
"version": "6", "version": "6",
"when": 1719109531147, "when": 1719547174326,
"tag": "0017_yummy_norrin_radd", "tag": "0017_minor_post",
"breakpoints": true "breakpoints": true
} }
] ]

View File

@@ -1,6 +1,6 @@
{ {
"name": "dokploy", "name": "dokploy",
"version": "v0.2.4", "version": "v0.2.5",
"private": true, "private": true,
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"type": "module", "type": "module",

29
pages/api/[...trpc].ts Normal file
View File

@@ -0,0 +1,29 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { createOpenApiNextHandler } from "@dokploy/trpc-openapi";
import { appRouter } from "@/server/api/root";
import { createTRPCContext } from "@/server/api/trpc";
import { validateBearerToken } from "@/server/auth/token";
import { validateRequest } from "@/server/auth/auth";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
let { session, user } = await validateBearerToken(req);
if (!session) {
const cookieResult = await validateRequest(req, res);
session = cookieResult.session;
user = cookieResult.user;
}
if (!user || !session) {
res.status(401).json({ message: "Unauthorized" });
return;
}
// @ts-ignore
return createOpenApiNextHandler({
router: appRouter,
createContext: createTRPCContext,
})(req, res);
};
export default handler;

View File

@@ -33,6 +33,7 @@ export default async function handler(
} }
const deploymentTitle = extractCommitMessage(req.headers, req.body); const deploymentTitle = extractCommitMessage(req.headers, req.body);
const deploymentHash = extractHash(req.headers, req.body);
const sourceType = application.sourceType; const sourceType = application.sourceType;
@@ -75,6 +76,7 @@ export default async function handler(
const jobData: DeploymentJob = { const jobData: DeploymentJob = {
applicationId: application.applicationId as string, applicationId: application.applicationId as string,
titleLog: deploymentTitle, titleLog: deploymentTitle,
descriptionLog: `Hash: ${deploymentHash}`,
type: "deploy", type: "deploy",
applicationType: "application", applicationType: "application",
}; };
@@ -166,6 +168,37 @@ export const extractCommitMessage = (headers: any, body: any) => {
return "NEW CHANGES"; return "NEW CHANGES";
}; };
export const extractHash = (headers: any, body: any) => {
// GitHub
if (headers["x-github-event"]) {
return body.head_commit ? body.head_commit.id : "";
}
// GitLab
if (headers["x-gitlab-event"]) {
return (
body.checkout_sha ||
(body.commits && body.commits.length > 0
? body.commits[0].id
: "NEW COMMIT")
);
}
// Bitbucket
if (headers["x-event-key"]?.includes("repo:push")) {
return body.push.changes && body.push.changes.length > 0
? body.push.changes[0].new.target.hash
: "NEW COMMIT";
}
// Gitea
if (headers["x-gitea-event"]) {
return body.after || "NEW COMMIT";
}
return "";
};
export const extractBranchName = (headers: any, body: any) => { export const extractBranchName = (headers: any, body: any) => {
if (headers["x-github-event"] || headers["x-gitea-event"]) { if (headers["x-github-event"] || headers["x-gitea-event"]) {
return body?.ref?.replace("refs/heads/", ""); return body?.ref?.replace("refs/heads/", "");

View File

@@ -4,7 +4,11 @@ import type { DeploymentJob } from "@/server/queues/deployments-queue";
import { myQueue } from "@/server/queues/queueSetup"; import { myQueue } from "@/server/queues/queueSetup";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import { extractBranchName, extractCommitMessage } from "../[refreshToken]"; import {
extractBranchName,
extractCommitMessage,
extractHash,
} from "../[refreshToken]";
import { updateCompose } from "@/server/api/services/compose"; import { updateCompose } from "@/server/api/services/compose";
export default async function handler( export default async function handler(
@@ -34,7 +38,7 @@ export default async function handler(
} }
const deploymentTitle = extractCommitMessage(req.headers, req.body); const deploymentTitle = extractCommitMessage(req.headers, req.body);
const deploymentHash = extractHash(req.headers, req.body);
const sourceType = composeResult.sourceType; const sourceType = composeResult.sourceType;
if (sourceType === "github") { if (sourceType === "github") {
@@ -61,6 +65,7 @@ export default async function handler(
titleLog: deploymentTitle, titleLog: deploymentTitle,
type: "deploy", type: "deploy",
applicationType: "compose", applicationType: "compose",
descriptionLog: `Hash: ${deploymentHash}`,
}; };
await myQueue.add( await myQueue.add(
"deployments", "deployments",

View File

@@ -6,15 +6,32 @@ import type { GetServerSidePropsContext, NextPage } from "next";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import "swagger-ui-react/swagger-ui.css"; import "swagger-ui-react/swagger-ui.css";
import superjson from "superjson"; import superjson from "superjson";
import { useEffect, useState } from "react";
const SwaggerUI = dynamic(() => import("swagger-ui-react"), { ssr: false }); const SwaggerUI = dynamic(() => import("swagger-ui-react"), { ssr: false });
const Home: NextPage = () => { const Home: NextPage = () => {
const { data } = api.settings.getOpenApiDocument.useQuery(); const { data } = api.settings.getOpenApiDocument.useQuery();
const [spec, setSpec] = useState({});
useEffect(() => {
// Esto solo se ejecutará en el cliente
if (data) {
const protocolAndHost = `${window.location.protocol}//${window.location.host}/api`;
const newSpec = {
...data,
servers: [{ url: protocolAndHost }],
externalDocs: {
url: `${protocolAndHost}/settings.getOpenApiDocument`,
},
};
setSpec(newSpec);
}
}, [data]);
return ( return (
<div className="h-screen bg-white"> <div className="h-screen bg-white">
<SwaggerUI spec={data || {}} /> <SwaggerUI spec={spec} />
</div> </div>
); );
}; };

85
pnpm-lock.yaml generated
View File

@@ -100,7 +100,7 @@ dependencies:
version: 10.45.2(@trpc/server@10.45.2) version: 10.45.2(@trpc/server@10.45.2)
'@trpc/next': '@trpc/next':
specifier: ^10.43.6 specifier: ^10.43.6
version: 10.45.2(@tanstack/react-query@4.36.1)(@trpc/client@10.45.2)(@trpc/react-query@10.45.2)(@trpc/server@10.45.2)(next@14.2.3)(react-dom@18.2.0)(react@18.2.0) version: 10.45.2(@tanstack/react-query@4.36.1)(@trpc/client@10.45.2)(@trpc/react-query@10.45.2)(@trpc/server@10.45.2)(next@14.2.4)(react-dom@18.2.0)(react@18.2.0)
'@trpc/react-query': '@trpc/react-query':
specifier: ^10.43.6 specifier: ^10.43.6
version: 10.45.2(@tanstack/react-query@4.36.1)(@trpc/client@10.45.2)(@trpc/server@10.45.2)(react-dom@18.2.0)(react@18.2.0) version: 10.45.2(@tanstack/react-query@4.36.1)(@trpc/client@10.45.2)(@trpc/server@10.45.2)(react-dom@18.2.0)(react@18.2.0)
@@ -193,10 +193,10 @@ dependencies:
version: 3.3.7 version: 3.3.7
next: next:
specifier: ^14.1.3 specifier: ^14.1.3
version: 14.2.3(react-dom@18.2.0)(react@18.2.0) version: 14.2.4(react-dom@18.2.0)(react@18.2.0)
next-themes: next-themes:
specifier: ^0.2.1 specifier: ^0.2.1
version: 0.2.1(next@14.2.3)(react-dom@18.2.0)(react@18.2.0) version: 0.2.1(next@14.2.4)(react-dom@18.2.0)(react@18.2.0)
node-os-utils: node-os-utils:
specifier: 1.3.7 specifier: 1.3.7
version: 1.3.7 version: 1.3.7
@@ -2059,12 +2059,12 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/env@14.2.3: /@next/env@14.2.4:
resolution: {integrity: sha512-W7fd7IbkfmeeY2gXrzJYDx8D2lWKbVoTIj1o1ScPHNzvp30s1AuoEFSdr39bC5sjxJaxTtq3OTCZboNp0lNWHA==} resolution: {integrity: sha512-3EtkY5VDkuV2+lNmKlbkibIJxcO4oIHEhBWne6PaAp+76J9KoSsGvNikp6ivzAT8dhhBMYrm6op2pS1ApG0Hzg==}
dev: false dev: false
/@next/swc-darwin-arm64@14.2.3: /@next/swc-darwin-arm64@14.2.4:
resolution: {integrity: sha512-3pEYo/RaGqPP0YzwnlmPN2puaF2WMLM3apt5jLW2fFdXD9+pqcoTzRk+iZsf8ta7+quAe4Q6Ms0nR0SFGFdS1A==} resolution: {integrity: sha512-AH3mO4JlFUqsYcwFUHb1wAKlebHU/Hv2u2kb1pAuRanDZ7pD/A/KPD98RHZmwsJpdHQwfEc/06mgpSzwrJYnNg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
@@ -2072,8 +2072,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-darwin-x64@14.2.3: /@next/swc-darwin-x64@14.2.4:
resolution: {integrity: sha512-6adp7waE6P1TYFSXpY366xwsOnEXM+y1kgRpjSRVI2CBDOcbRjsJ67Z6EgKIqWIue52d2q/Mx8g9MszARj8IEA==} resolution: {integrity: sha512-QVadW73sWIO6E2VroyUjuAxhWLZWEpiFqHdZdoQ/AMpN9YWGuHV8t2rChr0ahy+irKX5mlDU7OY68k3n4tAZTg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
@@ -2081,48 +2081,44 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-linux-arm64-gnu@14.2.3: /@next/swc-linux-arm64-gnu@14.2.4:
resolution: {integrity: sha512-cuzCE/1G0ZSnTAHJPUT1rPgQx1w5tzSX7POXSLaS7w2nIUJUD+e25QoXD/hMfxbsT9rslEXugWypJMILBj/QsA==} resolution: {integrity: sha512-KT6GUrb3oyCfcfJ+WliXuJnD6pCpZiosx2X3k66HLR+DMoilRb76LpWPGb4tZprawTtcnyrv75ElD6VncVamUQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
requiresBuild: true requiresBuild: true
dev: false dev: false
optional: true optional: true
/@next/swc-linux-arm64-musl@14.2.3: /@next/swc-linux-arm64-musl@14.2.4:
resolution: {integrity: sha512-0D4/oMM2Y9Ta3nGuCcQN8jjJjmDPYpHX9OJzqk42NZGJocU2MqhBq5tWkJrUQOQY9N+In9xOdymzapM09GeiZw==} resolution: {integrity: sha512-Alv8/XGSs/ytwQcbCHwze1HmiIkIVhDHYLjczSVrf0Wi2MvKn/blt7+S6FJitj3yTlMwMxII1gIJ9WepI4aZ/A==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
requiresBuild: true requiresBuild: true
dev: false dev: false
optional: true optional: true
/@next/swc-linux-x64-gnu@14.2.3: /@next/swc-linux-x64-gnu@14.2.4:
resolution: {integrity: sha512-ENPiNnBNDInBLyUU5ii8PMQh+4XLr4pG51tOp6aJ9xqFQ2iRI6IH0Ds2yJkAzNV1CfyagcyzPfROMViS2wOZ9w==} resolution: {integrity: sha512-ze0ShQDBPCqxLImzw4sCdfnB3lRmN3qGMB2GWDRlq5Wqy4G36pxtNOo2usu/Nm9+V2Rh/QQnrRc2l94kYFXO6Q==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
requiresBuild: true requiresBuild: true
dev: false dev: false
optional: true optional: true
/@next/swc-linux-x64-musl@14.2.3: /@next/swc-linux-x64-musl@14.2.4:
resolution: {integrity: sha512-BTAbq0LnCbF5MtoM7I/9UeUu/8ZBY0i8SFjUMCbPDOLv+un67e2JgyN4pmgfXBwy/I+RHu8q+k+MCkDN6P9ViQ==} resolution: {integrity: sha512-8dwC0UJoc6fC7PX70csdaznVMNr16hQrTDAMPvLPloazlcaWfdPogq+UpZX6Drqb1OBlwowz8iG7WR0Tzk/diQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
requiresBuild: true requiresBuild: true
dev: false dev: false
optional: true optional: true
/@next/swc-win32-arm64-msvc@14.2.3: /@next/swc-win32-arm64-msvc@14.2.4:
resolution: {integrity: sha512-AEHIw/dhAMLNFJFJIJIyOFDzrzI5bAjI9J26gbO5xhAKHYTZ9Or04BesFPXiAYXDNdrwTP2dQceYA4dL1geu8A==} resolution: {integrity: sha512-jxyg67NbEWkDyvM+O8UDbPAyYRZqGLQDTPwvrBBeOSyVWW/jFQkQKQ70JDqDSYg1ZDdl+E3nkbFbq8xM8E9x8A==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
@@ -2130,8 +2126,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-win32-ia32-msvc@14.2.3: /@next/swc-win32-ia32-msvc@14.2.4:
resolution: {integrity: sha512-vga40n1q6aYb0CLrM+eEmisfKCR45ixQYXuBXxOOmmoV8sYST9k7E3US32FsY+CkkF7NtzdcebiFT4CHuMSyZw==} resolution: {integrity: sha512-twrmN753hjXRdcrZmZttb/m5xaCBFa48Dt3FbeEItpJArxriYDunWxJn+QFXdJ3hPkm4u7CKxncVvnmgQMY1ag==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [ia32] cpu: [ia32]
os: [win32] os: [win32]
@@ -2139,8 +2135,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-win32-x64-msvc@14.2.3: /@next/swc-win32-x64-msvc@14.2.4:
resolution: {integrity: sha512-Q1/zm43RWynxrO7lW4ehciQVj+5ePBhOK+/K2P7pLFX3JaJ/IZVC69SHidrmZSOkqz7ECIOhhy7XhAFG4JYyHA==} resolution: {integrity: sha512-tkLrjBzqFTP8DVrAAQmZelEahfR9OxWpFR++vAI9FBhCiIxtwHwBHC23SBHCTURBtwB4kc/x44imVOnkKGNVGg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
@@ -4952,7 +4948,7 @@ packages:
'@trpc/server': 10.45.2 '@trpc/server': 10.45.2
dev: false dev: false
/@trpc/next@10.45.2(@tanstack/react-query@4.36.1)(@trpc/client@10.45.2)(@trpc/react-query@10.45.2)(@trpc/server@10.45.2)(next@14.2.3)(react-dom@18.2.0)(react@18.2.0): /@trpc/next@10.45.2(@tanstack/react-query@4.36.1)(@trpc/client@10.45.2)(@trpc/react-query@10.45.2)(@trpc/server@10.45.2)(next@14.2.4)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-RSORmfC+/nXdmRY1pQ0AalsVgSzwNAFbZLYHiTvPM5QQ8wmMEHilseCYMXpu0se/TbPt9zVR6Ka2d7O6zxKkXg==} resolution: {integrity: sha512-RSORmfC+/nXdmRY1pQ0AalsVgSzwNAFbZLYHiTvPM5QQ8wmMEHilseCYMXpu0se/TbPt9zVR6Ka2d7O6zxKkXg==}
peerDependencies: peerDependencies:
'@tanstack/react-query': ^4.18.0 '@tanstack/react-query': ^4.18.0
@@ -4967,7 +4963,7 @@ packages:
'@trpc/client': 10.45.2(@trpc/server@10.45.2) '@trpc/client': 10.45.2(@trpc/server@10.45.2)
'@trpc/react-query': 10.45.2(@tanstack/react-query@4.36.1)(@trpc/client@10.45.2)(@trpc/server@10.45.2)(react-dom@18.2.0)(react@18.2.0) '@trpc/react-query': 10.45.2(@tanstack/react-query@4.36.1)(@trpc/client@10.45.2)(@trpc/server@10.45.2)(react-dom@18.2.0)(react@18.2.0)
'@trpc/server': 10.45.2 '@trpc/server': 10.45.2
next: 14.2.3(react-dom@18.2.0)(react@18.2.0) next: 14.2.4(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0 react: 18.2.0
react-dom: 18.2.0(react@18.2.0) react-dom: 18.2.0(react@18.2.0)
dev: false dev: false
@@ -8104,14 +8100,14 @@ packages:
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
dev: false dev: false
/next-themes@0.2.1(next@14.2.3)(react-dom@18.2.0)(react@18.2.0): /next-themes@0.2.1(next@14.2.4)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==} resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==}
peerDependencies: peerDependencies:
next: '*' next: '*'
react: '*' react: '*'
react-dom: '*' react-dom: '*'
dependencies: dependencies:
next: 14.2.3(react-dom@18.2.0)(react@18.2.0) next: 14.2.4(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0 react: 18.2.0
react-dom: 18.2.0(react@18.2.0) react-dom: 18.2.0(react@18.2.0)
dev: false dev: false
@@ -8120,8 +8116,8 @@ packages:
resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==}
dev: true dev: true
/next@14.2.3(react-dom@18.2.0)(react@18.2.0): /next@14.2.4(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-dowFkFTR8v79NPJO4QsBUtxv0g9BrS/phluVpMAt2ku7H+cbcBJlopXjkWlwxrk/xGqMemr7JkGPGemPrLLX7A==} resolution: {integrity: sha512-R8/V7vugY+822rsQGQCjoLhMuC9oFj9SOi4Cl4b2wjDrseD0LRZ10W7R6Czo4w9ZznVSshKjuIomsRjvm9EKJQ==}
engines: {node: '>=18.17.0'} engines: {node: '>=18.17.0'}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@@ -8138,7 +8134,7 @@ packages:
sass: sass:
optional: true optional: true
dependencies: dependencies:
'@next/env': 14.2.3 '@next/env': 14.2.4
'@swc/helpers': 0.5.5 '@swc/helpers': 0.5.5
busboy: 1.6.0 busboy: 1.6.0
caniuse-lite: 1.0.30001598 caniuse-lite: 1.0.30001598
@@ -8148,15 +8144,15 @@ packages:
react-dom: 18.2.0(react@18.2.0) react-dom: 18.2.0(react@18.2.0)
styled-jsx: 5.1.1(react@18.2.0) styled-jsx: 5.1.1(react@18.2.0)
optionalDependencies: optionalDependencies:
'@next/swc-darwin-arm64': 14.2.3 '@next/swc-darwin-arm64': 14.2.4
'@next/swc-darwin-x64': 14.2.3 '@next/swc-darwin-x64': 14.2.4
'@next/swc-linux-arm64-gnu': 14.2.3 '@next/swc-linux-arm64-gnu': 14.2.4
'@next/swc-linux-arm64-musl': 14.2.3 '@next/swc-linux-arm64-musl': 14.2.4
'@next/swc-linux-x64-gnu': 14.2.3 '@next/swc-linux-x64-gnu': 14.2.4
'@next/swc-linux-x64-musl': 14.2.3 '@next/swc-linux-x64-musl': 14.2.4
'@next/swc-win32-arm64-msvc': 14.2.3 '@next/swc-win32-arm64-msvc': 14.2.4
'@next/swc-win32-ia32-msvc': 14.2.3 '@next/swc-win32-ia32-msvc': 14.2.4
'@next/swc-win32-x64-msvc': 14.2.3 '@next/swc-win32-x64-msvc': 14.2.4
transitivePeerDependencies: transitivePeerDependencies:
- '@babel/core' - '@babel/core'
- babel-plugin-macros - babel-plugin-macros
@@ -8551,7 +8547,7 @@ packages:
dependencies: dependencies:
nanoid: 3.3.7 nanoid: 3.3.7
picocolors: 1.0.0 picocolors: 1.0.0
source-map-js: 1.0.2 source-map-js: 1.2.0
dev: false dev: false
/postcss@8.4.35: /postcss@8.4.35:
@@ -9404,7 +9400,6 @@ packages:
/source-map-js@1.2.0: /source-map-js@1.2.0:
resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: true
/source-map-support@0.5.21: /source-map-support@0.5.21:
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}

View File

@@ -162,6 +162,7 @@ export const applicationRouter = createTRPCRouter({
const jobData: DeploymentJob = { const jobData: DeploymentJob = {
applicationId: input.applicationId, applicationId: input.applicationId,
titleLog: "Rebuild deployment", titleLog: "Rebuild deployment",
descriptionLog: "",
type: "redeploy", type: "redeploy",
applicationType: "application", applicationType: "application",
}; };
@@ -294,6 +295,7 @@ export const applicationRouter = createTRPCRouter({
const jobData: DeploymentJob = { const jobData: DeploymentJob = {
applicationId: input.applicationId, applicationId: input.applicationId,
titleLog: "Manual deployment", titleLog: "Manual deployment",
descriptionLog: "",
type: "deploy", type: "deploy",
applicationType: "application", applicationType: "application",
}; };

View File

@@ -138,6 +138,7 @@ export const composeRouter = createTRPCRouter({
titleLog: "Manual deployment", titleLog: "Manual deployment",
type: "deploy", type: "deploy",
applicationType: "compose", applicationType: "compose",
descriptionLog: "",
}; };
await myQueue.add( await myQueue.add(
"deployments", "deployments",
@@ -156,6 +157,7 @@ export const composeRouter = createTRPCRouter({
titleLog: "Rebuild deployment", titleLog: "Rebuild deployment",
type: "redeploy", type: "redeploy",
applicationType: "compose", applicationType: "compose",
descriptionLog: "",
}; };
await myQueue.add( await myQueue.add(
"deployments", "deployments",

View File

@@ -12,6 +12,8 @@ import {
createDomain, createDomain,
findDomainById, findDomainById,
findDomainsByApplicationId, findDomainsByApplicationId,
generateDomain,
generateWildcard,
removeDomainById, removeDomainById,
updateDomainById, updateDomainById,
} from "../services/domain"; } from "../services/domain";
@@ -35,6 +37,16 @@ export const domainRouter = createTRPCRouter({
.query(async ({ input }) => { .query(async ({ input }) => {
return await findDomainsByApplicationId(input.applicationId); return await findDomainsByApplicationId(input.applicationId);
}), }),
generateDomain: protectedProcedure
.input(apiFindDomainByApplication)
.mutation(async ({ input }) => {
return generateDomain(input);
}),
generateWildcard: protectedProcedure
.input(apiFindDomainByApplication)
.mutation(async ({ input }) => {
return generateWildcard(input);
}),
update: protectedProcedure update: protectedProcedure
.input(apiUpdateDomain) .input(apiUpdateDomain)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {

View File

@@ -248,7 +248,7 @@ export const settingsRouter = createTRPCRouter({
getOpenApiDocument: protectedProcedure.query( getOpenApiDocument: protectedProcedure.query(
async ({ ctx }): Promise<unknown> => { async ({ ctx }): Promise<unknown> => {
const protocol = ctx.req.headers["x-forwarded-proto"]; const protocol = ctx.req.headers["x-forwarded-proto"];
const url = `${protocol}://${ctx.req.headers.host}/api/trpc`; const url = `${protocol}://${ctx.req.headers.host}/api`;
const openApiDocument = generateOpenApiDocument(appRouter, { const openApiDocument = generateOpenApiDocument(appRouter, {
title: "tRPC OpenAPI", title: "tRPC OpenAPI",
version: "1.0.0", version: "1.0.0",

View File

@@ -130,15 +130,18 @@ export const updateApplicationStatus = async (
export const deployApplication = async ({ export const deployApplication = async ({
applicationId, applicationId,
titleLog = "Manual deployment", titleLog = "Manual deployment",
descriptionLog = "",
}: { }: {
applicationId: string; applicationId: string;
titleLog: string; titleLog: string;
descriptionLog: string;
}) => { }) => {
const application = await findApplicationById(applicationId); const application = await findApplicationById(applicationId);
const admin = await findAdmin(); const admin = await findAdmin();
const deployment = await createDeployment({ const deployment = await createDeployment({
applicationId: applicationId, applicationId: applicationId,
title: titleLog, title: titleLog,
description: descriptionLog,
}); });
try { try {
@@ -173,14 +176,17 @@ export const deployApplication = async ({
export const rebuildApplication = async ({ export const rebuildApplication = async ({
applicationId, applicationId,
titleLog = "Rebuild deployment", titleLog = "Rebuild deployment",
descriptionLog = "",
}: { }: {
applicationId: string; applicationId: string;
titleLog: string; titleLog: string;
descriptionLog: string;
}) => { }) => {
const application = await findApplicationById(applicationId); const application = await findApplicationById(applicationId);
const deployment = await createDeployment({ const deployment = await createDeployment({
applicationId: applicationId, applicationId: applicationId,
title: titleLog, title: titleLog,
description: descriptionLog,
}); });
try { try {

View File

@@ -134,15 +134,18 @@ export const updateCompose = async (
export const deployCompose = async ({ export const deployCompose = async ({
composeId, composeId,
titleLog = "Manual deployment", titleLog = "Manual deployment",
descriptionLog = "",
}: { }: {
composeId: string; composeId: string;
titleLog: string; titleLog: string;
descriptionLog: string;
}) => { }) => {
const compose = await findComposeById(composeId); const compose = await findComposeById(composeId);
const admin = await findAdmin(); const admin = await findAdmin();
const deployment = await createDeploymentCompose({ const deployment = await createDeploymentCompose({
composeId: composeId, composeId: composeId,
title: titleLog, title: titleLog,
description: descriptionLog,
}); });
try { try {
@@ -170,14 +173,17 @@ export const deployCompose = async ({
export const rebuildCompose = async ({ export const rebuildCompose = async ({
composeId, composeId,
titleLog = "Rebuild deployment", titleLog = "Rebuild deployment",
descriptionLog = "",
}: { }: {
composeId: string; composeId: string;
titleLog: string; titleLog: string;
descriptionLog: string;
}) => { }) => {
const compose = await findComposeById(composeId); const compose = await findComposeById(composeId);
const deployment = await createDeploymentCompose({ const deployment = await createDeploymentCompose({
composeId: composeId, composeId: composeId,
title: titleLog, title: titleLog,
description: descriptionLog,
}); });
try { try {

View File

@@ -60,6 +60,7 @@ export const createDeployment = async (
title: deployment.title || "Deployment", title: deployment.title || "Deployment",
status: "running", status: "running",
logPath: logFilePath, logPath: logFilePath,
description: deployment.description || "",
}) })
.returning(); .returning();
if (deploymentCreate.length === 0 || !deploymentCreate[0]) { if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
@@ -100,6 +101,7 @@ export const createDeploymentCompose = async (
.values({ .values({
composeId: deployment.composeId, composeId: deployment.composeId,
title: deployment.title || "Deployment", title: deployment.title || "Deployment",
description: deployment.description || "",
status: "running", status: "running",
logPath: logFilePath, logPath: logFilePath,
}) })

View File

@@ -1,9 +1,15 @@
import { db } from "@/server/db"; import { db } from "@/server/db";
import { type apiCreateDomain, domains } from "@/server/db/schema"; import {
type apiCreateDomain,
type apiFindDomainByApplication,
domains,
} from "@/server/db/schema";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { findApplicationById } from "./application"; import { findApplicationById } from "./application";
import { manageDomain } from "@/server/utils/traefik/domain"; import { manageDomain } from "@/server/utils/traefik/domain";
import { findAdmin } from "./admin";
import { generateRandomDomain } from "@/templates/utils";
export type Domain = typeof domains.$inferSelect; export type Domain = typeof domains.$inferSelect;
@@ -29,6 +35,58 @@ export const createDomain = async (input: typeof apiCreateDomain._type) => {
await manageDomain(application, domain); await manageDomain(application, domain);
}); });
}; };
export const generateDomain = async (
input: typeof apiFindDomainByApplication._type,
) => {
const application = await findApplicationById(input.applicationId);
const admin = await findAdmin();
const domain = await createDomain({
applicationId: application.applicationId,
host: generateRandomDomain({
serverIp: admin.serverIp || "",
projectName: application.appName,
}),
port: 3000,
certificateType: "none",
https: false,
path: "/",
});
return domain;
};
export const generateWildcard = async (
input: typeof apiFindDomainByApplication._type,
) => {
const application = await findApplicationById(input.applicationId);
const admin = await findAdmin();
if (!admin.host) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "We need a host to generate a wildcard domain",
});
}
const domain = await createDomain({
applicationId: application.applicationId,
host: generateWildcardDomain(application.appName, admin.host || ""),
port: 3000,
certificateType: "none",
https: false,
path: "/",
});
return domain;
};
export const generateWildcardDomain = (
appName: string,
serverDomain: string,
) => {
return `${appName}-${serverDomain}`;
};
export const findDomainById = async (domainId: string) => { export const findDomainById = async (domainId: string) => {
const domain = await db.query.domains.findFirst({ const domain = await db.query.domains.findFirst({
where: eq(domains.domainId, domainId), where: eq(domains.domainId, domainId),

View File

@@ -18,6 +18,7 @@ export const deployments = pgTable("deployment", {
.primaryKey() .primaryKey()
.$defaultFn(() => nanoid()), .$defaultFn(() => nanoid()),
title: text("title").notNull(), title: text("title").notNull(),
description: text("description"),
status: deploymentStatus("status").default("running"), status: deploymentStatus("status").default("running"),
logPath: text("logPath").notNull(), logPath: text("logPath").notNull(),
applicationId: text("applicationId").references( applicationId: text("applicationId").references(
@@ -49,6 +50,7 @@ const schema = createInsertSchema(deployments, {
logPath: z.string().min(1), logPath: z.string().min(1),
applicationId: z.string(), applicationId: z.string(),
composeId: z.string(), composeId: z.string(),
description: z.string().optional(),
}); });
export const apiCreateDeployment = schema export const apiCreateDeployment = schema
@@ -57,6 +59,7 @@ export const apiCreateDeployment = schema
status: true, status: true,
logPath: true, logPath: true,
applicationId: true, applicationId: true,
description: true,
}) })
.extend({ .extend({
applicationId: z.string().min(1), applicationId: z.string().min(1),
@@ -68,6 +71,7 @@ export const apiCreateDeploymentCompose = schema
status: true, status: true,
logPath: true, logPath: true,
composeId: true, composeId: true,
description: true,
}) })
.extend({ .extend({
composeId: z.string().min(1), composeId: z.string().min(1),

View File

@@ -10,12 +10,14 @@ type DeployJob =
| { | {
applicationId: string; applicationId: string;
titleLog: string; titleLog: string;
descriptionLog: string;
type: "deploy" | "redeploy"; type: "deploy" | "redeploy";
applicationType: "application"; applicationType: "application";
} }
| { | {
composeId: string; composeId: string;
titleLog: string; titleLog: string;
descriptionLog: string;
type: "deploy" | "redeploy"; type: "deploy" | "redeploy";
applicationType: "compose"; applicationType: "compose";
}; };
@@ -31,11 +33,13 @@ export const deploymentWorker = new Worker(
await rebuildApplication({ await rebuildApplication({
applicationId: job.data.applicationId, applicationId: job.data.applicationId,
titleLog: job.data.titleLog, titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog,
}); });
} else if (job.data.type === "deploy") { } else if (job.data.type === "deploy") {
await deployApplication({ await deployApplication({
applicationId: job.data.applicationId, applicationId: job.data.applicationId,
titleLog: job.data.titleLog, titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog,
}); });
} }
} else if (job.data.applicationType === "compose") { } else if (job.data.applicationType === "compose") {
@@ -43,11 +47,13 @@ export const deploymentWorker = new Worker(
await deployCompose({ await deployCompose({
composeId: job.data.composeId, composeId: job.data.composeId,
titleLog: job.data.titleLog, titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog,
}); });
} else if (job.data.type === "redeploy") { } else if (job.data.type === "redeploy") {
await rebuildCompose({ await rebuildCompose({
composeId: job.data.composeId, composeId: job.data.composeId,
titleLog: job.data.titleLog, titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog,
}); });
} }
} }

View File

@@ -63,6 +63,9 @@ export const buildMariadb = async (mariadb: MariadbWithMounts) => {
Resources: { Resources: {
...resources, ...resources,
}, },
Placement: {
Constraints: ["node.role==manager"],
},
}, },
Mode: { Mode: {
Replicated: { Replicated: {

View File

@@ -63,6 +63,9 @@ export const buildMongo = async (mongo: MongoWithMounts) => {
Resources: { Resources: {
...resources, ...resources,
}, },
Placement: {
Constraints: ["node.role==manager"],
},
}, },
Mode: { Mode: {
Replicated: { Replicated: {

View File

@@ -69,6 +69,9 @@ export const buildMysql = async (mysql: MysqlWithMounts) => {
Resources: { Resources: {
...resources, ...resources,
}, },
Placement: {
Constraints: ["node.role==manager"],
},
}, },
Mode: { Mode: {
Replicated: { Replicated: {

View File

@@ -63,6 +63,9 @@ export const buildPostgres = async (postgres: PostgresWithMounts) => {
Resources: { Resources: {
...resources, ...resources,
}, },
Placement: {
Constraints: ["node.role==manager"],
},
}, },
Mode: { Mode: {
Replicated: { Replicated: {

View File

@@ -50,17 +50,19 @@ export const buildRedis = async (redis: RedisWithMounts) => {
Image: dockerImage, Image: dockerImage,
Env: envVariables, Env: envVariables,
Mounts: [...volumesMount, ...bindsMount, ...filesMount], Mounts: [...volumesMount, ...bindsMount, ...filesMount],
...(command Command: ["/bin/sh"],
? { Args: [
Command: ["/bin/sh"], "-c",
Args: ["-c", command], command ? command : `redis-server --requirepass ${databasePassword}`,
} ],
: {}),
}, },
Networks: [{ Target: "dokploy-network" }], Networks: [{ Target: "dokploy-network" }],
Resources: { Resources: {
...resources, ...resources,
}, },
Placement: {
Constraints: ["node.role==manager"],
},
}, },
Mode: { Mode: {
Replicated: { Replicated: {