mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Docker compose support (#111)
* feat(WIP): compose implementation * feat: add volumes, networks, services name hash generate * feat: add compose config test unique * feat: add tests for each unique config * feat: implement lodash for docker compose parsing * feat: add tests for generating compose file * refactor: implement logs docker compose * refactor: composeFile set not empty * feat: implement providers for compose deployments * feat: add Files volumes to compose * feat: add stop compose button * refactor: change strategie of building compose * feat: create .env file in composepath * refactor: simplify git and github function * chore: update deps * refactor: update migrations and add badge to recognize compose type * chore: update lock yaml * refactor: use code editor * feat: add monitoring for app types * refactor: reset stats on change appName * refactor: add option to clean monitoring folder * feat: show current command that will run * feat: add prefix * fix: add missing types * refactor: add docker provider and expose by default as false * refactor: customize error page * refactor: unified deployments to be a single one * feat: add vitest to ci/cd * revert: back to initial version * refactor: add maxconcurrency vitest * refactor: add pool forks to vitest * feat: add pocketbase template * fix: update path resolution compose * removed * feat: add template pocketbase * feat: add pocketbase template * feat: add support button * feat: add plausible template * feat: add calcom template * feat: add version to each template * feat: add code editor to enviroment variables and swarm settings json * refactor: add loader when download the image * fix: use base64 to generate keys plausible * feat: add recognized domain names by enviroment compose * refactor: show alert to redeploy in each card advanced tab * refactor: add validation to prevent create compose if not have permissions * chore: add templates section to contributing * chore: add example contributing
This commit is contained in:
@@ -20,6 +20,7 @@ import { securityRouter } from "./routers/security";
|
||||
import { portRouter } from "./routers/port";
|
||||
import { adminRouter } from "./routers/admin";
|
||||
import { dockerRouter } from "./routers/docker";
|
||||
import { composeRouter } from "./routers/compose";
|
||||
import { registryRouter } from "./routers/registry";
|
||||
import { clusterRouter } from "./routers/cluster";
|
||||
/**
|
||||
@@ -49,6 +50,7 @@ export const appRouter = createTRPCRouter({
|
||||
security: securityRouter,
|
||||
redirects: redirectsRouter,
|
||||
port: portRouter,
|
||||
compose: composeRouter,
|
||||
registry: registryRouter,
|
||||
cluster: clusterRouter,
|
||||
});
|
||||
|
||||
@@ -160,6 +160,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
applicationId: input.applicationId,
|
||||
titleLog: "Rebuild deployment",
|
||||
type: "redeploy",
|
||||
applicationType: "application",
|
||||
};
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
@@ -291,6 +292,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
applicationId: input.applicationId,
|
||||
titleLog: "Manual deployment",
|
||||
type: "deploy",
|
||||
applicationType: "application",
|
||||
};
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
|
||||
278
server/api/routers/compose.ts
Normal file
278
server/api/routers/compose.ts
Normal file
@@ -0,0 +1,278 @@
|
||||
import {
|
||||
apiCreateCompose,
|
||||
apiCreateComposeByTemplate,
|
||||
apiFindCompose,
|
||||
apiRandomizeCompose,
|
||||
apiUpdateCompose,
|
||||
compose,
|
||||
} from "@/server/db/schema";
|
||||
import {
|
||||
createCompose,
|
||||
createComposeByTemplate,
|
||||
findComposeById,
|
||||
loadServices,
|
||||
removeCompose,
|
||||
stopCompose,
|
||||
updateCompose,
|
||||
} from "../services/compose";
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
import { addNewService, checkServiceAccess } from "../services/user";
|
||||
import {
|
||||
cleanQueuesByCompose,
|
||||
type DeploymentJob,
|
||||
} from "@/server/queues/deployments-queue";
|
||||
import { myQueue } from "@/server/queues/queueSetup";
|
||||
import {
|
||||
generateSSHKey,
|
||||
readRSAFile,
|
||||
removeRSAFiles,
|
||||
} from "@/server/utils/filesystem/ssh";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "@/server/db";
|
||||
import { randomizeComposeFile } from "@/server/utils/docker/compose";
|
||||
import { nanoid } from "nanoid";
|
||||
import { removeDeploymentsByComposeId } from "../services/deployment";
|
||||
import { removeComposeDirectory } from "@/server/utils/filesystem/directory";
|
||||
import { createCommand } from "@/server/utils/builders/compose";
|
||||
import { loadTemplateModule, readComposeFile } from "@/templates/utils";
|
||||
import { findAdmin } from "../services/admin";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { findProjectById, slugifyProjectName } from "../services/project";
|
||||
import { createMount } from "../services/mount";
|
||||
import type { TemplatesKeys } from "@/templates/types/templates-data.type";
|
||||
import { templates } from "@/templates/templates";
|
||||
|
||||
export const composeRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.input(apiCreateCompose)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
try {
|
||||
if (ctx.user.rol === "user") {
|
||||
await checkServiceAccess(ctx.user.authId, input.projectId, "create");
|
||||
}
|
||||
const newService = await createCompose(input);
|
||||
|
||||
if (ctx.user.rol === "user") {
|
||||
await addNewService(ctx.user.authId, newService.composeId);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the compose",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
one: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (ctx.user.rol === "user") {
|
||||
await checkServiceAccess(ctx.user.authId, input.composeId, "access");
|
||||
}
|
||||
|
||||
return await findComposeById(input.composeId);
|
||||
}),
|
||||
|
||||
update: protectedProcedure
|
||||
.input(apiUpdateCompose)
|
||||
.mutation(async ({ input }) => {
|
||||
return updateCompose(input.composeId, input);
|
||||
}),
|
||||
delete: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (ctx.user.rol === "user") {
|
||||
await checkServiceAccess(ctx.user.authId, input.composeId, "delete");
|
||||
}
|
||||
const composeResult = await findComposeById(input.composeId);
|
||||
|
||||
const result = await db
|
||||
.delete(compose)
|
||||
.where(eq(compose.composeId, input.composeId))
|
||||
.returning();
|
||||
|
||||
const cleanupOperations = [
|
||||
async () => await removeCompose(composeResult),
|
||||
async () => await removeDeploymentsByComposeId(composeResult),
|
||||
async () => await removeComposeDirectory(composeResult.appName),
|
||||
async () => await removeRSAFiles(composeResult.appName),
|
||||
];
|
||||
|
||||
for (const operation of cleanupOperations) {
|
||||
try {
|
||||
await operation();
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
return result[0];
|
||||
}),
|
||||
cleanQueues: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input }) => {
|
||||
await cleanQueuesByCompose(input.composeId);
|
||||
}),
|
||||
|
||||
allServices: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.query(async ({ input }) => {
|
||||
return await loadServices(input.composeId);
|
||||
}),
|
||||
|
||||
randomizeCompose: protectedProcedure
|
||||
.input(apiRandomizeCompose)
|
||||
.mutation(async ({ input }) => {
|
||||
return await randomizeComposeFile(input.composeId, input.prefix);
|
||||
}),
|
||||
|
||||
deploy: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input }) => {
|
||||
const jobData: DeploymentJob = {
|
||||
composeId: input.composeId,
|
||||
titleLog: "Manual deployment",
|
||||
type: "deploy",
|
||||
applicationType: "compose",
|
||||
};
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
{ ...jobData },
|
||||
{
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
},
|
||||
);
|
||||
}),
|
||||
redeploy: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input }) => {
|
||||
const jobData: DeploymentJob = {
|
||||
composeId: input.composeId,
|
||||
titleLog: "Rebuild deployment",
|
||||
type: "redeploy",
|
||||
applicationType: "compose",
|
||||
};
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
{ ...jobData },
|
||||
{
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
},
|
||||
);
|
||||
}),
|
||||
stop: protectedProcedure.input(apiFindCompose).mutation(async ({ input }) => {
|
||||
await stopCompose(input.composeId);
|
||||
|
||||
return true;
|
||||
}),
|
||||
getDefaultCommand: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.query(async ({ input }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
const command = createCommand(compose);
|
||||
return `docker ${command}`;
|
||||
}),
|
||||
generateSSHKey: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
try {
|
||||
await generateSSHKey(compose.appName);
|
||||
const file = await readRSAFile(compose.appName);
|
||||
|
||||
await updateCompose(input.composeId, {
|
||||
customGitSSHKey: file,
|
||||
});
|
||||
} catch (error) {}
|
||||
|
||||
return true;
|
||||
}),
|
||||
refreshToken: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input }) => {
|
||||
await updateCompose(input.composeId, {
|
||||
refreshToken: nanoid(),
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
removeSSHKey: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
await removeRSAFiles(compose.appName);
|
||||
await updateCompose(input.composeId, {
|
||||
customGitSSHKey: null,
|
||||
});
|
||||
|
||||
return true;
|
||||
}),
|
||||
deployTemplate: protectedProcedure
|
||||
.input(apiCreateComposeByTemplate)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
if (ctx.user.rol === "user") {
|
||||
await checkServiceAccess(ctx.user.authId, input.projectId, "create");
|
||||
}
|
||||
const composeFile = await readComposeFile(input.id);
|
||||
|
||||
const generate = await loadTemplateModule(input.id as TemplatesKeys);
|
||||
|
||||
const admin = await findAdmin();
|
||||
|
||||
if (!admin.serverIp) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message:
|
||||
"You need to have a server IP to deploy this template in order to generate domains",
|
||||
});
|
||||
}
|
||||
|
||||
const project = await findProjectById(input.projectId);
|
||||
|
||||
const projectName = slugifyProjectName(`${project.name}-${input.id}`);
|
||||
const { envs, mounts } = generate({
|
||||
serverIp: admin.serverIp,
|
||||
projectName: projectName,
|
||||
});
|
||||
|
||||
const compose = await createComposeByTemplate({
|
||||
...input,
|
||||
composeFile: composeFile,
|
||||
env: envs.join("\n"),
|
||||
name: input.id,
|
||||
sourceType: "raw",
|
||||
});
|
||||
|
||||
if (ctx.user.rol === "user") {
|
||||
await addNewService(ctx.user.authId, compose.composeId);
|
||||
}
|
||||
|
||||
if (mounts && mounts?.length > 0) {
|
||||
for (const mount of mounts) {
|
||||
await createMount({
|
||||
mountPath: mount.mountPath,
|
||||
content: mount.content,
|
||||
serviceId: compose.composeId,
|
||||
serviceType: "compose",
|
||||
type: "file",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}),
|
||||
|
||||
templates: protectedProcedure.query(async () => {
|
||||
const templatesData = templates.map((t) => ({
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
id: t.id,
|
||||
links: t.links,
|
||||
tags: t.tags,
|
||||
logo: t.logo,
|
||||
version: t.version,
|
||||
}));
|
||||
|
||||
return templatesData;
|
||||
}),
|
||||
});
|
||||
@@ -1,11 +1,23 @@
|
||||
import { apiFindAllByApplication } from "@/server/db/schema";
|
||||
import { findAllDeploymentsByApplicationId } from "../services/deployment";
|
||||
import {
|
||||
apiFindAllByApplication,
|
||||
apiFindAllByCompose,
|
||||
} from "@/server/db/schema";
|
||||
import {
|
||||
findAllDeploymentsByApplicationId,
|
||||
findAllDeploymentsByComposeId,
|
||||
} from "../services/deployment";
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
|
||||
export const deploymentRouter = createTRPCRouter({
|
||||
all: protectedProcedure
|
||||
.input(apiFindAllByApplication)
|
||||
.query(async ({ input }) => {
|
||||
return await findAllDeploymentsByApplicationId(input.applicationId);
|
||||
}),
|
||||
all: protectedProcedure
|
||||
.input(apiFindAllByApplication)
|
||||
.query(async ({ input }) => {
|
||||
return await findAllDeploymentsByApplicationId(input.applicationId);
|
||||
}),
|
||||
|
||||
allByCompose: protectedProcedure
|
||||
.input(apiFindAllByCompose)
|
||||
.query(async ({ input }) => {
|
||||
return await findAllDeploymentsByComposeId(input.composeId);
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import { apiCreateMount, apiRemoveMount } from "@/server/db/schema";
|
||||
import { createMount, deleteMount } from "../services/mount";
|
||||
import {
|
||||
apiCreateMount,
|
||||
apiFindOneMount,
|
||||
apiRemoveMount,
|
||||
apiUpdateMount,
|
||||
} from "@/server/db/schema";
|
||||
import {
|
||||
createMount,
|
||||
deleteMount,
|
||||
findMountById,
|
||||
updateMount,
|
||||
} from "../services/mount";
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
|
||||
export const mountRouter = createTRPCRouter({
|
||||
@@ -14,4 +24,14 @@ export const mountRouter = createTRPCRouter({
|
||||
.mutation(async ({ input }) => {
|
||||
return await deleteMount(input.mountId);
|
||||
}),
|
||||
|
||||
one: protectedProcedure.input(apiFindOneMount).query(async ({ input }) => {
|
||||
return await findMountById(input.mountId);
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.input(apiUpdateMount)
|
||||
.mutation(async ({ input }) => {
|
||||
await updateMount(input.mountId, input);
|
||||
return true;
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
} from "../services/user";
|
||||
import {
|
||||
applications,
|
||||
compose,
|
||||
mariadb,
|
||||
mongo,
|
||||
mysql,
|
||||
@@ -64,6 +65,9 @@ export const projectRouter = createTRPCRouter({
|
||||
const service = await db.query.projects.findFirst({
|
||||
where: eq(projects.projectId, input.projectId),
|
||||
with: {
|
||||
compose: {
|
||||
where: buildServiceFilter(compose.composeId, accesedServices),
|
||||
},
|
||||
applications: {
|
||||
where: buildServiceFilter(
|
||||
applications.applicationId,
|
||||
@@ -135,6 +139,9 @@ export const projectRouter = createTRPCRouter({
|
||||
redis: {
|
||||
where: buildServiceFilter(redis.redisId, accesedServices),
|
||||
},
|
||||
compose: {
|
||||
where: buildServiceFilter(compose.composeId, accesedServices),
|
||||
},
|
||||
},
|
||||
orderBy: desc(projects.createdAt),
|
||||
});
|
||||
@@ -149,6 +156,7 @@ export const projectRouter = createTRPCRouter({
|
||||
mysql: true,
|
||||
postgres: true,
|
||||
redis: true,
|
||||
compose: true,
|
||||
},
|
||||
orderBy: desc(projects.createdAt),
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { docker, MAIN_TRAEFIK_PATH } from "@/server/constants";
|
||||
import { docker, MAIN_TRAEFIK_PATH, MONITORING_PATH } from "@/server/constants";
|
||||
import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
import {
|
||||
cleanStoppedContainers,
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
readDirectory,
|
||||
} from "../services/settings";
|
||||
import { canAccessToTraefikFiles } from "../services/user";
|
||||
import { recreateDirectory } from "@/server/utils/filesystem/directory";
|
||||
|
||||
export const settingsRouter = createTRPCRouter({
|
||||
reloadServer: adminProcedure.mutation(async () => {
|
||||
@@ -85,6 +86,10 @@ export const settingsRouter = createTRPCRouter({
|
||||
await cleanUpSystemPrune();
|
||||
return true;
|
||||
}),
|
||||
cleanMonitoring: adminProcedure.mutation(async () => {
|
||||
await recreateDirectory(MONITORING_PATH);
|
||||
return true;
|
||||
}),
|
||||
saveSSHPrivateKey: adminProcedure
|
||||
.input(apiSaveSSHKey)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
|
||||
215
server/api/services/compose.ts
Normal file
215
server/api/services/compose.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
import { db } from "@/server/db";
|
||||
import { type apiCreateCompose, compose } from "@/server/db/schema";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { load } from "js-yaml";
|
||||
import { findAdmin } from "./admin";
|
||||
import { createDeploymentCompose, updateDeploymentStatus } from "./deployment";
|
||||
import { buildCompose } from "@/server/utils/builders/compose";
|
||||
import { createComposeFile } from "@/server/utils/providers/raw";
|
||||
import { execAsync } from "@/server/utils/process/execAsync";
|
||||
import { join } from "node:path";
|
||||
import { COMPOSE_PATH } from "@/server/constants";
|
||||
import { cloneGithubRepository } from "@/server/utils/providers/github";
|
||||
import { cloneGitRepository } from "@/server/utils/providers/git";
|
||||
|
||||
export type Compose = typeof compose.$inferSelect;
|
||||
|
||||
export const createCompose = async (input: typeof apiCreateCompose._type) => {
|
||||
const newDestination = await db
|
||||
.insert(compose)
|
||||
.values({
|
||||
...input,
|
||||
composeFile: "",
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newDestination) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting compose",
|
||||
});
|
||||
}
|
||||
|
||||
return newDestination;
|
||||
};
|
||||
|
||||
export const createComposeByTemplate = async (
|
||||
input: typeof compose.$inferInsert,
|
||||
) => {
|
||||
const newDestination = await db
|
||||
.insert(compose)
|
||||
.values({
|
||||
...input,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newDestination) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting compose",
|
||||
});
|
||||
}
|
||||
|
||||
return newDestination;
|
||||
};
|
||||
|
||||
export const findComposeById = async (composeId: string) => {
|
||||
const result = await db.query.compose.findFirst({
|
||||
where: eq(compose.composeId, composeId),
|
||||
with: {
|
||||
project: true,
|
||||
deployments: true,
|
||||
mounts: true,
|
||||
},
|
||||
});
|
||||
if (!result) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Compose not found",
|
||||
});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const loadServices = async (composeId: string) => {
|
||||
const compose = await findComposeById(composeId);
|
||||
|
||||
// use js-yaml to parse the docker compose file and then extact the services
|
||||
const composeFile = compose.composeFile;
|
||||
const composeData = load(composeFile) as ComposeSpecification;
|
||||
|
||||
if (!composeData?.services) {
|
||||
return ["All Services"];
|
||||
}
|
||||
|
||||
const services = Object.keys(composeData.services);
|
||||
|
||||
return [...services, "All Services"];
|
||||
};
|
||||
|
||||
export const updateCompose = async (
|
||||
composeId: string,
|
||||
composeData: Partial<Compose>,
|
||||
) => {
|
||||
const composeResult = await db
|
||||
.update(compose)
|
||||
.set({
|
||||
...composeData,
|
||||
})
|
||||
.where(eq(compose.composeId, composeId))
|
||||
.returning();
|
||||
|
||||
return composeResult[0];
|
||||
};
|
||||
|
||||
export const deployCompose = async ({
|
||||
composeId,
|
||||
titleLog = "Manual deployment",
|
||||
}: {
|
||||
composeId: string;
|
||||
titleLog: string;
|
||||
}) => {
|
||||
const compose = await findComposeById(composeId);
|
||||
const admin = await findAdmin();
|
||||
const deployment = await createDeploymentCompose({
|
||||
composeId: composeId,
|
||||
title: titleLog,
|
||||
});
|
||||
|
||||
try {
|
||||
if (compose.sourceType === "github") {
|
||||
await cloneGithubRepository(admin, compose, deployment.logPath, true);
|
||||
} else if (compose.sourceType === "git") {
|
||||
await cloneGitRepository(compose, deployment.logPath, true);
|
||||
} else {
|
||||
await createComposeFile(compose, deployment.logPath);
|
||||
}
|
||||
await buildCompose(compose, deployment.logPath);
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updateCompose(composeId, {
|
||||
composeStatus: "done",
|
||||
});
|
||||
} catch (error) {
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
await updateCompose(composeId, {
|
||||
composeStatus: "error",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const rebuildCompose = async ({
|
||||
composeId,
|
||||
titleLog = "Rebuild deployment",
|
||||
}: {
|
||||
composeId: string;
|
||||
titleLog: string;
|
||||
}) => {
|
||||
const compose = await findComposeById(composeId);
|
||||
const deployment = await createDeploymentCompose({
|
||||
composeId: composeId,
|
||||
title: titleLog,
|
||||
});
|
||||
|
||||
try {
|
||||
await buildCompose(compose, deployment.logPath);
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updateCompose(composeId, {
|
||||
composeStatus: "done",
|
||||
});
|
||||
} catch (error) {
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
await updateCompose(composeId, {
|
||||
composeStatus: "error",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const removeCompose = async (compose: Compose) => {
|
||||
try {
|
||||
const projectPath = join(COMPOSE_PATH, compose.appName);
|
||||
|
||||
if (compose.composeType === "stack") {
|
||||
await execAsync(`docker stack rm ${compose.appName}`, {
|
||||
cwd: projectPath,
|
||||
});
|
||||
} else {
|
||||
await execAsync(`docker compose -p ${compose.appName} down`, {
|
||||
cwd: projectPath,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const stopCompose = async (composeId: string) => {
|
||||
const compose = await findComposeById(composeId);
|
||||
try {
|
||||
if (compose.composeType === "docker-compose") {
|
||||
await execAsync(`docker compose -p ${compose.appName} stop`, {
|
||||
cwd: join(COMPOSE_PATH, compose.appName),
|
||||
});
|
||||
}
|
||||
|
||||
await updateCompose(composeId, {
|
||||
composeStatus: "idle",
|
||||
});
|
||||
} catch (error) {
|
||||
await updateCompose(composeId, {
|
||||
composeStatus: "error",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
@@ -2,12 +2,17 @@ import { existsSync, promises as fsPromises } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { LOGS_PATH } from "@/server/constants";
|
||||
import { db } from "@/server/db";
|
||||
import { deployments } from "@/server/db/schema";
|
||||
import {
|
||||
type apiCreateDeployment,
|
||||
type apiCreateDeploymentCompose,
|
||||
deployments,
|
||||
} from "@/server/db/schema";
|
||||
import { removeDirectoryIfExistsContent } from "@/server/utils/filesystem/directory";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { format } from "date-fns";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import { type Application, findApplicationById } from "./application";
|
||||
import { type Compose, findComposeById } from "./compose";
|
||||
|
||||
export type Deployment = typeof deployments.$inferSelect;
|
||||
type CreateDeploymentInput = Omit<
|
||||
@@ -31,7 +36,12 @@ export const findDeploymentById = async (applicationId: string) => {
|
||||
return application;
|
||||
};
|
||||
|
||||
export const createDeployment = async (deployment: CreateDeploymentInput) => {
|
||||
export const createDeployment = async (
|
||||
deployment: Omit<
|
||||
typeof apiCreateDeployment._type,
|
||||
"deploymentId" | "createdAt" | "status" | "logPath"
|
||||
>,
|
||||
) => {
|
||||
try {
|
||||
const application = await findApplicationById(deployment.applicationId);
|
||||
|
||||
@@ -68,6 +78,48 @@ export const createDeployment = async (deployment: CreateDeploymentInput) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const createDeploymentCompose = async (
|
||||
deployment: Omit<
|
||||
typeof apiCreateDeploymentCompose._type,
|
||||
"deploymentId" | "createdAt" | "status" | "logPath"
|
||||
>,
|
||||
) => {
|
||||
try {
|
||||
const compose = await findComposeById(deployment.composeId);
|
||||
|
||||
await removeLastTenComposeDeployments(deployment.composeId);
|
||||
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
||||
const fileName = `${compose.appName}-${formattedDateTime}.log`;
|
||||
const logFilePath = path.join(LOGS_PATH, compose.appName, fileName);
|
||||
await fsPromises.mkdir(path.join(LOGS_PATH, compose.appName), {
|
||||
recursive: true,
|
||||
});
|
||||
await fsPromises.writeFile(logFilePath, "Initializing deployment");
|
||||
const deploymentCreate = await db
|
||||
.insert(deployments)
|
||||
.values({
|
||||
composeId: deployment.composeId,
|
||||
title: deployment.title || "Deployment",
|
||||
status: "running",
|
||||
logPath: logFilePath,
|
||||
})
|
||||
.returning();
|
||||
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the deployment",
|
||||
});
|
||||
}
|
||||
return deploymentCreate[0];
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the deployment",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const removeDeployment = async (deploymentId: string) => {
|
||||
try {
|
||||
const deployment = await db
|
||||
@@ -109,6 +161,23 @@ const removeLastTenDeployments = async (applicationId: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const removeLastTenComposeDeployments = async (composeId: string) => {
|
||||
const deploymentList = await db.query.deployments.findMany({
|
||||
where: eq(deployments.composeId, composeId),
|
||||
orderBy: desc(deployments.createdAt),
|
||||
});
|
||||
if (deploymentList.length > 10) {
|
||||
const deploymentsToDelete = deploymentList.slice(10);
|
||||
for (const oldDeployment of deploymentsToDelete) {
|
||||
const logPath = path.join(oldDeployment.logPath);
|
||||
if (existsSync(logPath)) {
|
||||
await fsPromises.unlink(logPath);
|
||||
}
|
||||
await removeDeployment(oldDeployment.deploymentId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const removeDeployments = async (application: Application) => {
|
||||
const { appName, applicationId } = application;
|
||||
const logsPath = path.join(LOGS_PATH, appName);
|
||||
@@ -116,6 +185,16 @@ export const removeDeployments = async (application: Application) => {
|
||||
await removeDeploymentsByApplicationId(applicationId);
|
||||
};
|
||||
|
||||
export const removeDeploymentsByComposeId = async (compose: Compose) => {
|
||||
const { appName } = compose;
|
||||
const logsPath = path.join(LOGS_PATH, appName);
|
||||
await removeDirectoryIfExistsContent(logsPath);
|
||||
await db
|
||||
.delete(deployments)
|
||||
.where(eq(deployments.composeId, compose.composeId))
|
||||
.returning();
|
||||
};
|
||||
|
||||
export const findAllDeploymentsByApplicationId = async (
|
||||
applicationId: string,
|
||||
) => {
|
||||
@@ -126,6 +205,14 @@ export const findAllDeploymentsByApplicationId = async (
|
||||
return deploymentsList;
|
||||
};
|
||||
|
||||
export const findAllDeploymentsByComposeId = async (composeId: string) => {
|
||||
const deploymentsList = await db.query.deployments.findMany({
|
||||
where: eq(deployments.composeId, composeId),
|
||||
orderBy: desc(deployments.createdAt),
|
||||
});
|
||||
return deploymentsList;
|
||||
};
|
||||
|
||||
export const updateDeployment = async (
|
||||
deploymentId: string,
|
||||
deploymentData: Partial<Deployment>,
|
||||
|
||||
@@ -77,8 +77,7 @@ export const getContainersByAppNameMatch = async (appName: string) => {
|
||||
);
|
||||
|
||||
if (stderr) {
|
||||
console.error(`Error: ${stderr}`);
|
||||
return;
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!stdout) return [];
|
||||
|
||||
@@ -37,6 +37,9 @@ export const createMount = async (input: typeof apiCreateMount._type) => {
|
||||
...(input.serviceType === "redis" && {
|
||||
redisId: serviceId,
|
||||
}),
|
||||
...(input.serviceType === "compose" && {
|
||||
composeId: serviceId,
|
||||
}),
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
@@ -49,6 +52,7 @@ export const createMount = async (input: typeof apiCreateMount._type) => {
|
||||
}
|
||||
return value;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the mount",
|
||||
@@ -80,12 +84,12 @@ export const findMountById = async (mountId: string) => {
|
||||
|
||||
export const updateMount = async (
|
||||
mountId: string,
|
||||
applicationData: Partial<Mount>,
|
||||
mountData: Partial<Mount>,
|
||||
) => {
|
||||
const mount = await db
|
||||
.update(mounts)
|
||||
.set({
|
||||
...applicationData,
|
||||
...mountData,
|
||||
})
|
||||
.where(eq(mounts.mountId, mountId))
|
||||
.returning();
|
||||
|
||||
@@ -37,6 +37,7 @@ export const findProjectById = async (projectId: string) => {
|
||||
mysql: true,
|
||||
postgres: true,
|
||||
redis: true,
|
||||
compose: true,
|
||||
},
|
||||
});
|
||||
if (!project) {
|
||||
@@ -73,3 +74,13 @@ export const updateProjectById = async (
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const slugifyProjectName = (projectName: string): string => {
|
||||
return projectName
|
||||
.toLowerCase()
|
||||
.replace(/[0-9]/g, "")
|
||||
.replace(/[^a-z\s-]/g, "")
|
||||
.replace(/\s+/g, "-")
|
||||
.replace(/-+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
};
|
||||
|
||||
@@ -21,7 +21,12 @@ export const getDokployImage = () => {
|
||||
|
||||
export const pullLatestRelease = async () => {
|
||||
try {
|
||||
await docker.pull(getDokployImage(), {});
|
||||
const stream = await docker.pull(getDokployImage(), {});
|
||||
await new Promise((resolve, reject) => {
|
||||
docker.modem.followProgress(stream, (err, res) =>
|
||||
err ? reject(err) : resolve(res),
|
||||
);
|
||||
});
|
||||
const newUpdateIsAvailable = await updateIsAvailable();
|
||||
return newUpdateIsAvailable;
|
||||
} catch (error) {}
|
||||
|
||||
Reference in New Issue
Block a user