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) {}
|
||||
|
||||
@@ -9,6 +9,7 @@ export const MAIN_TRAEFIK_PATH = `${BASE_PATH}/traefik`;
|
||||
export const DYNAMIC_TRAEFIK_PATH = `${BASE_PATH}/traefik/dynamic`;
|
||||
export const LOGS_PATH = `${BASE_PATH}/logs`;
|
||||
export const APPLICATIONS_PATH = `${BASE_PATH}/applications`;
|
||||
export const COMPOSE_PATH = `${BASE_PATH}/compose`;
|
||||
export const SSH_PATH = `${BASE_PATH}/ssh`;
|
||||
export const CERTIFICATES_PATH = `${DYNAMIC_TRAEFIK_PATH}/certificates`;
|
||||
export const REGISTRY_PATH = `${DYNAMIC_TRAEFIK_PATH}/registry`;
|
||||
|
||||
109
server/db/schema/compose.ts
Normal file
109
server/db/schema/compose.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { z } from "zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { boolean, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { projects } from "./project";
|
||||
import { relations } from "drizzle-orm";
|
||||
import { deployments } from "./deployment";
|
||||
import { generateAppName } from "./utils";
|
||||
import { applicationStatus } from "./shared";
|
||||
import { mounts } from "./mount";
|
||||
|
||||
export const sourceTypeCompose = pgEnum("sourceTypeCompose", [
|
||||
"git",
|
||||
"github",
|
||||
"raw",
|
||||
]);
|
||||
|
||||
export const composeType = pgEnum("composeType", ["docker-compose", "stack"]);
|
||||
|
||||
export const compose = pgTable("compose", {
|
||||
composeId: text("composeId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
name: text("name").notNull(),
|
||||
appName: text("appName")
|
||||
.notNull()
|
||||
.$defaultFn(() => generateAppName("compose")),
|
||||
description: text("description"),
|
||||
env: text("env"),
|
||||
composeFile: text("composeFile").notNull().default(""),
|
||||
refreshToken: text("refreshToken").$defaultFn(() => nanoid()),
|
||||
sourceType: sourceTypeCompose("sourceType").notNull().default("github"),
|
||||
composeType: composeType("composeType").notNull().default("docker-compose"),
|
||||
// Github
|
||||
repository: text("repository"),
|
||||
owner: text("owner"),
|
||||
branch: text("branch"),
|
||||
autoDeploy: boolean("autoDeploy"),
|
||||
// Git
|
||||
customGitUrl: text("customGitUrl"),
|
||||
customGitBranch: text("customGitBranch"),
|
||||
customGitSSHKey: text("customGitSSHKey"),
|
||||
//
|
||||
command: text("command").notNull().default(""),
|
||||
//
|
||||
composePath: text("composePath").notNull().default("./docker-compose.yml"),
|
||||
composeStatus: applicationStatus("composeStatus").notNull().default("idle"),
|
||||
projectId: text("projectId")
|
||||
.notNull()
|
||||
.references(() => projects.projectId, { onDelete: "cascade" }),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
});
|
||||
|
||||
export const composeRelations = relations(compose, ({ one, many }) => ({
|
||||
project: one(projects, {
|
||||
fields: [compose.projectId],
|
||||
references: [projects.projectId],
|
||||
}),
|
||||
deployments: many(deployments),
|
||||
mounts: many(mounts),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(compose, {
|
||||
name: z.string().min(1),
|
||||
description: z.string(),
|
||||
env: z.string().optional(),
|
||||
composeFile: z.string().min(1),
|
||||
projectId: z.string(),
|
||||
command: z.string().optional(),
|
||||
composePath: z.string().min(1),
|
||||
composeType: z.enum(["docker-compose", "stack"]).optional(),
|
||||
});
|
||||
|
||||
export const apiCreateCompose = createSchema.pick({
|
||||
name: true,
|
||||
description: true,
|
||||
projectId: true,
|
||||
composeType: true,
|
||||
});
|
||||
|
||||
export const apiCreateComposeByTemplate = createSchema
|
||||
.pick({
|
||||
projectId: true,
|
||||
})
|
||||
.extend({
|
||||
id: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiFindCompose = z.object({
|
||||
composeId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiUpdateCompose = createSchema.partial().extend({
|
||||
composeId: z.string(),
|
||||
composeFile: z.string().optional(),
|
||||
command: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiRandomizeCompose = createSchema
|
||||
.pick({
|
||||
composeId: true,
|
||||
})
|
||||
.extend({
|
||||
prefix: z.string().optional(),
|
||||
composeId: z.string().min(1),
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import { nanoid } from "nanoid";
|
||||
import { applications } from "./application";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { pgEnum, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { compose } from "./compose";
|
||||
|
||||
export const deploymentStatus = pgEnum("deploymentStatus", [
|
||||
"running",
|
||||
@@ -19,9 +20,13 @@ export const deployments = pgTable("deployment", {
|
||||
title: text("title").notNull(),
|
||||
status: deploymentStatus("status").default("running"),
|
||||
logPath: text("logPath").notNull(),
|
||||
applicationId: text("applicationId")
|
||||
.notNull()
|
||||
.references(() => applications.applicationId, { onDelete: "cascade" }),
|
||||
applicationId: text("applicationId").references(
|
||||
() => applications.applicationId,
|
||||
{ onDelete: "cascade" },
|
||||
),
|
||||
composeId: text("composeId").references(() => compose.composeId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
@@ -32,13 +37,18 @@ export const deploymentsRelations = relations(deployments, ({ one }) => ({
|
||||
fields: [deployments.applicationId],
|
||||
references: [applications.applicationId],
|
||||
}),
|
||||
compose: one(compose, {
|
||||
fields: [deployments.composeId],
|
||||
references: [compose.composeId],
|
||||
}),
|
||||
}));
|
||||
|
||||
const schema = createInsertSchema(deployments, {
|
||||
title: z.string().min(1),
|
||||
status: z.string().default("running"),
|
||||
logPath: z.string().min(1),
|
||||
applicationId: z.string().min(1),
|
||||
applicationId: z.string(),
|
||||
composeId: z.string(),
|
||||
});
|
||||
|
||||
export const apiCreateDeployment = schema
|
||||
@@ -48,10 +58,35 @@ export const apiCreateDeployment = schema
|
||||
logPath: true,
|
||||
applicationId: true,
|
||||
})
|
||||
.required();
|
||||
.extend({
|
||||
applicationId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiCreateDeploymentCompose = schema
|
||||
.pick({
|
||||
title: true,
|
||||
status: true,
|
||||
logPath: true,
|
||||
composeId: true,
|
||||
})
|
||||
.extend({
|
||||
composeId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiFindAllByApplication = schema
|
||||
.pick({
|
||||
applicationId: true,
|
||||
})
|
||||
.extend({
|
||||
applicationId: z.string().min(1),
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiFindAllByCompose = schema
|
||||
.pick({
|
||||
composeId: true,
|
||||
})
|
||||
.extend({
|
||||
composeId: z.string().min(1),
|
||||
})
|
||||
.required();
|
||||
|
||||
@@ -19,4 +19,5 @@ export * from "./security";
|
||||
export * from "./port";
|
||||
export * from "./redis";
|
||||
export * from "./shared";
|
||||
export * from "./compose";
|
||||
export * from "./registry";
|
||||
|
||||
@@ -9,6 +9,7 @@ import { mariadb } from "./mariadb";
|
||||
import { mongo } from "./mongo";
|
||||
import { mysql } from "./mysql";
|
||||
import { redis } from "./redis";
|
||||
import { compose } from "./compose";
|
||||
|
||||
export const serviceType = pgEnum("serviceType", [
|
||||
"application",
|
||||
@@ -17,6 +18,7 @@ export const serviceType = pgEnum("serviceType", [
|
||||
"mariadb",
|
||||
"mongo",
|
||||
"redis",
|
||||
"compose",
|
||||
]);
|
||||
|
||||
export const mountType = pgEnum("mountType", ["bind", "volume", "file"]);
|
||||
@@ -51,6 +53,9 @@ export const mounts = pgTable("mount", {
|
||||
redisId: text("redisId").references(() => redis.redisId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
composeId: text("composeId").references(() => compose.composeId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
});
|
||||
|
||||
export const MountssRelations = relations(mounts, ({ one }) => ({
|
||||
@@ -78,6 +83,10 @@ export const MountssRelations = relations(mounts, ({ one }) => ({
|
||||
fields: [mounts.redisId],
|
||||
references: [redis.redisId],
|
||||
}),
|
||||
compose: one(compose, {
|
||||
fields: [mounts.composeId],
|
||||
references: [compose.composeId],
|
||||
}),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(mounts, {
|
||||
@@ -89,7 +98,15 @@ const createSchema = createInsertSchema(mounts, {
|
||||
mountPath: z.string().min(1),
|
||||
mountId: z.string().optional(),
|
||||
serviceType: z
|
||||
.enum(["application", "postgres", "mysql", "mariadb", "mongo", "redis"])
|
||||
.enum([
|
||||
"application",
|
||||
"postgres",
|
||||
"mysql",
|
||||
"mariadb",
|
||||
"mongo",
|
||||
"redis",
|
||||
"compose",
|
||||
])
|
||||
.default("application"),
|
||||
});
|
||||
|
||||
@@ -134,3 +151,7 @@ export const apiFindMountByApplicationId = createSchema
|
||||
serviceType: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiUpdateMount = createSchema.partial().extend({
|
||||
mountId: z.string().min(1),
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import { applications } from "./application";
|
||||
import { mongo } from "./mongo";
|
||||
import { redis } from "./redis";
|
||||
import { admins } from "./admin";
|
||||
import { compose } from "./compose";
|
||||
|
||||
export const projects = pgTable("project", {
|
||||
projectId: text("projectId")
|
||||
@@ -34,6 +35,7 @@ export const projectRelations = relations(projects, ({ many, one }) => ({
|
||||
applications: many(applications),
|
||||
mongo: many(mongo),
|
||||
redis: many(redis),
|
||||
compose: many(compose),
|
||||
admin: one(admins, {
|
||||
fields: [projects.adminId],
|
||||
references: [admins.adminId],
|
||||
|
||||
@@ -4,12 +4,21 @@ import {
|
||||
rebuildApplication,
|
||||
} from "../api/services/application";
|
||||
import { myQueue, redisConfig } from "./queueSetup";
|
||||
import { deployCompose, rebuildCompose } from "../api/services/compose";
|
||||
|
||||
interface DeployJob {
|
||||
applicationId: string;
|
||||
titleLog: string;
|
||||
type: "deploy" | "redeploy";
|
||||
}
|
||||
type DeployJob =
|
||||
| {
|
||||
applicationId: string;
|
||||
titleLog: string;
|
||||
type: "deploy" | "redeploy";
|
||||
applicationType: "application";
|
||||
}
|
||||
| {
|
||||
composeId: string;
|
||||
titleLog: string;
|
||||
type: "deploy" | "redeploy";
|
||||
applicationType: "compose";
|
||||
};
|
||||
|
||||
export type DeploymentJob = DeployJob;
|
||||
|
||||
@@ -17,16 +26,30 @@ export const deploymentWorker = new Worker(
|
||||
"deployments",
|
||||
async (job: Job<DeploymentJob>) => {
|
||||
try {
|
||||
if (job.data.type === "redeploy") {
|
||||
await rebuildApplication({
|
||||
applicationId: job.data.applicationId,
|
||||
titleLog: job.data.titleLog,
|
||||
});
|
||||
} else if (job.data.type === "deploy") {
|
||||
await deployApplication({
|
||||
applicationId: job.data.applicationId,
|
||||
titleLog: job.data.titleLog,
|
||||
});
|
||||
if (job.data.applicationType === "application") {
|
||||
if (job.data.type === "redeploy") {
|
||||
await rebuildApplication({
|
||||
applicationId: job.data.applicationId,
|
||||
titleLog: job.data.titleLog,
|
||||
});
|
||||
} else if (job.data.type === "deploy") {
|
||||
await deployApplication({
|
||||
applicationId: job.data.applicationId,
|
||||
titleLog: job.data.titleLog,
|
||||
});
|
||||
}
|
||||
} else if (job.data.applicationType === "compose") {
|
||||
if (job.data.type === "deploy") {
|
||||
await deployCompose({
|
||||
composeId: job.data.composeId,
|
||||
titleLog: job.data.titleLog,
|
||||
});
|
||||
} else if (job.data.type === "redeploy") {
|
||||
await rebuildCompose({
|
||||
composeId: job.data.composeId,
|
||||
titleLog: job.data.titleLog,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
@@ -42,9 +65,20 @@ export const cleanQueuesByApplication = async (applicationId: string) => {
|
||||
const jobs = await myQueue.getJobs(["waiting", "delayed"]);
|
||||
|
||||
for (const job of jobs) {
|
||||
if (job.data.applicationId === applicationId) {
|
||||
if (job?.data?.applicationId === applicationId) {
|
||||
await job.remove();
|
||||
console.log(`Removed job ${job.id} for application ${applicationId}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const cleanQueuesByCompose = async (composeId: string) => {
|
||||
const jobs = await myQueue.getJobs(["waiting", "delayed"]);
|
||||
|
||||
for (const job of jobs) {
|
||||
if (job?.data?.composeId === composeId) {
|
||||
await job.remove();
|
||||
console.log(`Removed job ${job.id} for compose ${composeId}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -127,11 +127,18 @@ export const createDefaultTraefikConfig = () => {
|
||||
}
|
||||
const configObject: MainTraefikConfig = {
|
||||
providers: {
|
||||
...(process.env.NODE_ENV === "development" && {
|
||||
docker: {
|
||||
defaultRule: "Host(`{{ trimPrefix `/` .Name }}.docker.localhost`)",
|
||||
},
|
||||
}),
|
||||
...(process.env.NODE_ENV === "development"
|
||||
? {
|
||||
docker: {
|
||||
defaultRule:
|
||||
"Host(`{{ trimPrefix `/` .Name }}.docker.localhost`)",
|
||||
},
|
||||
}
|
||||
: {
|
||||
docker: {
|
||||
exposedByDefault: false,
|
||||
},
|
||||
}),
|
||||
file: {
|
||||
directory: "/etc/dokploy/traefik/dynamic",
|
||||
watch: true,
|
||||
@@ -140,17 +147,6 @@ export const createDefaultTraefikConfig = () => {
|
||||
entryPoints: {
|
||||
web: {
|
||||
address: ":80",
|
||||
...(process.env.NODE_ENV === "production" && {
|
||||
http: {
|
||||
redirections: {
|
||||
entryPoint: {
|
||||
to: "websecure",
|
||||
scheme: "https",
|
||||
permanent: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
websecure: {
|
||||
address: ":443",
|
||||
|
||||
117
server/utils/builders/compose.ts
Normal file
117
server/utils/builders/compose.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import {
|
||||
createWriteStream,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import type { InferResultType } from "@/server/types/with";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
import { COMPOSE_PATH } from "@/server/constants";
|
||||
import { dirname, join } from "node:path";
|
||||
import {
|
||||
generateFileMountsCompose,
|
||||
prepareEnvironmentVariables,
|
||||
} from "../docker/utils";
|
||||
import boxen from "boxen";
|
||||
|
||||
export type ComposeNested = InferResultType<
|
||||
"compose",
|
||||
{ project: true; mounts: true }
|
||||
>;
|
||||
export const buildCompose = async (compose: ComposeNested, logPath: string) => {
|
||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||
const { sourceType, appName, mounts, composeType, env, composePath } =
|
||||
compose;
|
||||
try {
|
||||
const command = createCommand(compose);
|
||||
generateFileMountsCompose(appName, mounts);
|
||||
|
||||
createEnvFile(compose);
|
||||
|
||||
const logContent = `
|
||||
App Name: ${appName}
|
||||
Build Compose 🐳
|
||||
Detected: ${mounts.length} mounts 📂
|
||||
Command: docker ${command}
|
||||
Source Type: docker ${sourceType} ✅
|
||||
Compose Type: ${composeType} ✅`;
|
||||
const logBox = boxen(logContent, {
|
||||
padding: {
|
||||
left: 1,
|
||||
right: 1,
|
||||
bottom: 1,
|
||||
},
|
||||
width: 80,
|
||||
borderStyle: "double",
|
||||
});
|
||||
writeStream.write(`\n${logBox}\n`);
|
||||
|
||||
const projectPath = join(COMPOSE_PATH, compose.appName);
|
||||
await spawnAsync(
|
||||
"docker",
|
||||
[...command.split(" ")],
|
||||
(data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data.toString());
|
||||
}
|
||||
},
|
||||
{
|
||||
cwd: projectPath,
|
||||
},
|
||||
);
|
||||
|
||||
writeStream.write("Docker Compose Deployed: ✅");
|
||||
} catch (error) {
|
||||
writeStream.write(`ERROR: ${error}: ❌`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
}
|
||||
};
|
||||
|
||||
const sanitizeCommand = (command: string) => {
|
||||
const sanitizedCommand = command.trim();
|
||||
|
||||
const parts = sanitizedCommand.split(/\s+/);
|
||||
|
||||
const restCommand = parts.map((arg) => arg.replace(/^"(.*)"$/, "$1"));
|
||||
|
||||
return restCommand.join(" ");
|
||||
};
|
||||
|
||||
export const createCommand = (compose: ComposeNested) => {
|
||||
const { composeType, appName, sourceType } = compose;
|
||||
|
||||
const path =
|
||||
sourceType === "raw" ? "docker-compose.yml" : compose.composePath;
|
||||
let command = "";
|
||||
|
||||
if (composeType === "docker-compose") {
|
||||
command = `compose -p ${appName} -f ${path} up -d --build --remove-orphans`;
|
||||
} else if (composeType === "stack") {
|
||||
command = `stack deploy -c ${path} ${appName} --prune`;
|
||||
}
|
||||
|
||||
const customCommand = sanitizeCommand(compose.command);
|
||||
|
||||
if (customCommand) {
|
||||
command = `${command} ${customCommand}`;
|
||||
}
|
||||
|
||||
return command;
|
||||
};
|
||||
|
||||
const createEnvFile = (compose: ComposeNested) => {
|
||||
const { env, composePath, appName } = compose;
|
||||
const composeFilePath =
|
||||
join(COMPOSE_PATH, appName, composePath) ||
|
||||
join(COMPOSE_PATH, appName, "docker-compose.yml");
|
||||
|
||||
const envFilePath = join(dirname(composeFilePath), ".env");
|
||||
const envFileContent = prepareEnvironmentVariables(env).join("\n");
|
||||
|
||||
if (!existsSync(dirname(envFilePath))) {
|
||||
mkdirSync(dirname(envFilePath), { recursive: true });
|
||||
}
|
||||
writeFileSync(envFilePath, envFileContent);
|
||||
};
|
||||
45
server/utils/docker/compose.ts
Normal file
45
server/utils/docker/compose.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import crypto from "node:crypto";
|
||||
import type { ComposeSpecification } from "./types";
|
||||
import { findComposeById } from "@/server/api/services/compose";
|
||||
import { dump, load } from "js-yaml";
|
||||
import { addPrefixToAllVolumes } from "./compose/volume";
|
||||
import { addPrefixToAllConfigs } from "./compose/configs";
|
||||
import { addPrefixToAllNetworks } from "./compose/network";
|
||||
import { addPrefixToAllSecrets } from "./compose/secrets";
|
||||
import { addPrefixToAllServiceNames } from "./compose/service";
|
||||
|
||||
export const generateRandomHash = (): string => {
|
||||
return crypto.randomBytes(4).toString("hex");
|
||||
};
|
||||
|
||||
export const randomizeComposeFile = async (
|
||||
composeId: string,
|
||||
prefix?: string,
|
||||
) => {
|
||||
const compose = await findComposeById(composeId);
|
||||
const composeFile = compose.composeFile;
|
||||
const composeData = load(composeFile) as ComposeSpecification;
|
||||
|
||||
const randomPrefix = prefix || generateRandomHash();
|
||||
|
||||
const newComposeFile = addPrefixToAllProperties(composeData, randomPrefix);
|
||||
|
||||
return dump(newComposeFile);
|
||||
};
|
||||
|
||||
export const addPrefixToAllProperties = (
|
||||
composeData: ComposeSpecification,
|
||||
prefix: string,
|
||||
): ComposeSpecification => {
|
||||
let updatedComposeData = { ...composeData };
|
||||
|
||||
updatedComposeData = addPrefixToAllServiceNames(updatedComposeData, prefix);
|
||||
|
||||
updatedComposeData = addPrefixToAllVolumes(updatedComposeData, prefix);
|
||||
|
||||
updatedComposeData = addPrefixToAllNetworks(updatedComposeData, prefix);
|
||||
updatedComposeData = addPrefixToAllConfigs(updatedComposeData, prefix);
|
||||
|
||||
updatedComposeData = addPrefixToAllSecrets(updatedComposeData, prefix);
|
||||
return updatedComposeData;
|
||||
};
|
||||
73
server/utils/docker/compose/configs.ts
Normal file
73
server/utils/docker/compose/configs.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import _ from "lodash";
|
||||
import type {
|
||||
ComposeSpecification,
|
||||
DefinitionsConfig,
|
||||
DefinitionsService,
|
||||
} from "../types";
|
||||
|
||||
export const addPrefixToConfigsRoot = (
|
||||
configs: { [key: string]: DefinitionsConfig },
|
||||
prefix: string,
|
||||
): { [key: string]: DefinitionsConfig } => {
|
||||
const newConfigs: { [key: string]: DefinitionsConfig } = {};
|
||||
|
||||
_.forEach(configs, (config, configName) => {
|
||||
const newConfigName = `${configName}-${prefix}`;
|
||||
newConfigs[newConfigName] = _.cloneDeep(config);
|
||||
});
|
||||
|
||||
return newConfigs;
|
||||
};
|
||||
|
||||
export const addPrefixToConfigsInServices = (
|
||||
services: { [key: string]: DefinitionsService },
|
||||
prefix: string,
|
||||
): { [key: string]: DefinitionsService } => {
|
||||
const newServices: { [key: string]: DefinitionsService } = {};
|
||||
|
||||
_.forEach(services, (serviceConfig, serviceName) => {
|
||||
const newServiceConfig = _.cloneDeep(serviceConfig);
|
||||
|
||||
// Reemplazar nombres de configs en configs
|
||||
if (_.has(newServiceConfig, "configs")) {
|
||||
newServiceConfig.configs = _.map(newServiceConfig.configs, (config) => {
|
||||
if (_.isString(config)) {
|
||||
return `${config}-${prefix}`;
|
||||
}
|
||||
if (_.isObject(config) && config.source) {
|
||||
return {
|
||||
...config,
|
||||
source: `${config.source}-${prefix}`,
|
||||
};
|
||||
}
|
||||
return config;
|
||||
});
|
||||
}
|
||||
|
||||
newServices[serviceName] = newServiceConfig;
|
||||
});
|
||||
|
||||
return newServices;
|
||||
};
|
||||
|
||||
export const addPrefixToAllConfigs = (
|
||||
composeData: ComposeSpecification,
|
||||
prefix: string,
|
||||
): ComposeSpecification => {
|
||||
const updatedComposeData = { ...composeData };
|
||||
if (composeData?.configs) {
|
||||
updatedComposeData.configs = addPrefixToConfigsRoot(
|
||||
composeData.configs,
|
||||
prefix,
|
||||
);
|
||||
}
|
||||
|
||||
if (composeData?.services) {
|
||||
updatedComposeData.services = addPrefixToConfigsInServices(
|
||||
composeData.services,
|
||||
prefix,
|
||||
);
|
||||
}
|
||||
|
||||
return updatedComposeData;
|
||||
};
|
||||
72
server/utils/docker/compose/network.ts
Normal file
72
server/utils/docker/compose/network.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import _ from "lodash";
|
||||
import type {
|
||||
ComposeSpecification,
|
||||
DefinitionsNetwork,
|
||||
DefinitionsService,
|
||||
} from "../types";
|
||||
|
||||
export const addPrefixToNetworksRoot = (
|
||||
networks: { [key: string]: DefinitionsNetwork },
|
||||
prefix: string,
|
||||
): { [key: string]: DefinitionsNetwork } => {
|
||||
return _.mapKeys(networks, (_value, key) => `${key}-${prefix}`);
|
||||
};
|
||||
|
||||
export const addPrefixToServiceNetworks = (
|
||||
services: { [key: string]: DefinitionsService },
|
||||
prefix: string,
|
||||
): { [key: string]: DefinitionsService } => {
|
||||
return _.mapValues(services, (service) => {
|
||||
if (service.networks) {
|
||||
// 1 Case the most common
|
||||
if (Array.isArray(service.networks)) {
|
||||
service.networks = service.networks.map(
|
||||
(network: string) => `${network}-${prefix}`,
|
||||
);
|
||||
} else {
|
||||
// 2 Case
|
||||
service.networks = _.mapKeys(
|
||||
service.networks,
|
||||
(_value, key) => `${key}-${prefix}`,
|
||||
);
|
||||
|
||||
// 3 Case
|
||||
service.networks = _.mapValues(service.networks, (value) => {
|
||||
if (value && typeof value === "object") {
|
||||
return _.mapKeys(value, (_val, innerKey) => {
|
||||
if (innerKey === "aliases") {
|
||||
return "aliases";
|
||||
}
|
||||
return `${innerKey}-${prefix}`;
|
||||
});
|
||||
}
|
||||
return value;
|
||||
});
|
||||
}
|
||||
}
|
||||
return service;
|
||||
});
|
||||
};
|
||||
|
||||
export const addPrefixToAllNetworks = (
|
||||
composeData: ComposeSpecification,
|
||||
prefix: string,
|
||||
): ComposeSpecification => {
|
||||
const updatedComposeData = { ...composeData };
|
||||
|
||||
if (updatedComposeData.networks) {
|
||||
updatedComposeData.networks = addPrefixToNetworksRoot(
|
||||
updatedComposeData.networks,
|
||||
prefix,
|
||||
);
|
||||
}
|
||||
|
||||
if (updatedComposeData.services) {
|
||||
updatedComposeData.services = addPrefixToServiceNetworks(
|
||||
updatedComposeData.services,
|
||||
prefix,
|
||||
);
|
||||
}
|
||||
|
||||
return updatedComposeData;
|
||||
};
|
||||
68
server/utils/docker/compose/secrets.ts
Normal file
68
server/utils/docker/compose/secrets.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import _ from "lodash";
|
||||
import type { ComposeSpecification, DefinitionsService } from "../types";
|
||||
|
||||
export const addPrefixToSecretsRoot = (
|
||||
secrets: ComposeSpecification["secrets"],
|
||||
prefix: string,
|
||||
): ComposeSpecification["secrets"] => {
|
||||
const newSecrets: ComposeSpecification["secrets"] = {};
|
||||
_.forEach(secrets, (secretConfig, secretName) => {
|
||||
const newSecretName = `${secretName}-${prefix}`;
|
||||
newSecrets[newSecretName] = _.cloneDeep(secretConfig);
|
||||
});
|
||||
return newSecrets;
|
||||
};
|
||||
|
||||
export const addPrefixToSecretsInServices = (
|
||||
services: { [key: string]: DefinitionsService },
|
||||
prefix: string,
|
||||
): { [key: string]: DefinitionsService } => {
|
||||
const newServices: { [key: string]: DefinitionsService } = {};
|
||||
|
||||
_.forEach(services, (serviceConfig, serviceName) => {
|
||||
const newServiceConfig = _.cloneDeep(serviceConfig);
|
||||
|
||||
// Replace secret names in secrets
|
||||
if (_.has(newServiceConfig, "secrets")) {
|
||||
newServiceConfig.secrets = _.map(newServiceConfig.secrets, (secret) => {
|
||||
if (_.isString(secret)) {
|
||||
return `${secret}-${prefix}`;
|
||||
}
|
||||
if (_.isObject(secret) && secret.source) {
|
||||
return {
|
||||
...secret,
|
||||
source: `${secret.source}-${prefix}`,
|
||||
};
|
||||
}
|
||||
return secret;
|
||||
});
|
||||
}
|
||||
|
||||
newServices[serviceName] = newServiceConfig;
|
||||
});
|
||||
|
||||
return newServices;
|
||||
};
|
||||
|
||||
export const addPrefixToAllSecrets = (
|
||||
composeData: ComposeSpecification,
|
||||
prefix: string,
|
||||
): ComposeSpecification => {
|
||||
const updatedComposeData = { ...composeData };
|
||||
|
||||
if (composeData?.secrets) {
|
||||
updatedComposeData.secrets = addPrefixToSecretsRoot(
|
||||
composeData.secrets,
|
||||
prefix,
|
||||
);
|
||||
}
|
||||
|
||||
if (composeData?.services) {
|
||||
updatedComposeData.services = addPrefixToSecretsInServices(
|
||||
composeData.services,
|
||||
prefix,
|
||||
);
|
||||
}
|
||||
|
||||
return updatedComposeData;
|
||||
};
|
||||
90
server/utils/docker/compose/service.ts
Normal file
90
server/utils/docker/compose/service.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
// En la sección depends_on de otros servicios: Para definir dependencias entre servicios.
|
||||
// En la sección networks de otros servicios: Aunque esto no es común, es posible referenciar servicios en redes personalizadas.
|
||||
// En la sección volumes_from de otros servicios: Para reutilizar volúmenes definidos por otro servicio.
|
||||
// En la sección links de otros servicios: Para crear enlaces entre servicios.
|
||||
// En la sección extends de otros servicios: Para extender la configuración de otro servicio.
|
||||
|
||||
import _ from "lodash";
|
||||
import type { ComposeSpecification, DefinitionsService } from "../types";
|
||||
type DependsOnObject = NonNullable<
|
||||
Exclude<DefinitionsService["depends_on"], string[]> extends infer T
|
||||
? { [K in keyof T]: T[K] }
|
||||
: never
|
||||
>;
|
||||
|
||||
export const addPrefixToServiceNames = (
|
||||
services: { [key: string]: DefinitionsService },
|
||||
prefix: string,
|
||||
): { [key: string]: DefinitionsService } => {
|
||||
const newServices: { [key: string]: DefinitionsService } = {};
|
||||
|
||||
for (const [serviceName, serviceConfig] of Object.entries(services)) {
|
||||
const newServiceName = `${serviceName}-${prefix}`;
|
||||
const newServiceConfig = _.cloneDeep(serviceConfig);
|
||||
|
||||
// Reemplazar nombres de servicios en depends_on
|
||||
if (newServiceConfig.depends_on) {
|
||||
if (Array.isArray(newServiceConfig.depends_on)) {
|
||||
newServiceConfig.depends_on = newServiceConfig.depends_on.map(
|
||||
(dep) => `${dep}-${prefix}`,
|
||||
);
|
||||
} else {
|
||||
const newDependsOn: DependsOnObject = {};
|
||||
for (const [depName, depConfig] of Object.entries(
|
||||
newServiceConfig.depends_on,
|
||||
)) {
|
||||
newDependsOn[`${depName}-${prefix}`] = depConfig;
|
||||
}
|
||||
newServiceConfig.depends_on = newDependsOn;
|
||||
}
|
||||
}
|
||||
|
||||
// Reemplazar nombre en container_name
|
||||
if (newServiceConfig.container_name) {
|
||||
newServiceConfig.container_name = `${newServiceConfig.container_name}-${prefix}`;
|
||||
}
|
||||
|
||||
// Reemplazar nombres de servicios en links
|
||||
if (newServiceConfig.links) {
|
||||
newServiceConfig.links = newServiceConfig.links.map(
|
||||
(link) => `${link}-${prefix}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Reemplazar nombres de servicios en extends
|
||||
if (newServiceConfig.extends) {
|
||||
if (typeof newServiceConfig.extends === "string") {
|
||||
newServiceConfig.extends = `${newServiceConfig.extends}-${prefix}`;
|
||||
} else {
|
||||
newServiceConfig.extends.service = `${newServiceConfig.extends.service}-${prefix}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Reemplazar nombres de servicios en volumes_from
|
||||
if (newServiceConfig.volumes_from) {
|
||||
newServiceConfig.volumes_from = newServiceConfig.volumes_from.map(
|
||||
(vol) => `${vol}-${prefix}`,
|
||||
);
|
||||
}
|
||||
|
||||
newServices[newServiceName] = newServiceConfig;
|
||||
}
|
||||
|
||||
return newServices;
|
||||
};
|
||||
|
||||
export const addPrefixToAllServiceNames = (
|
||||
composeData: ComposeSpecification,
|
||||
prefix: string,
|
||||
): ComposeSpecification => {
|
||||
const updatedComposeData = { ...composeData };
|
||||
|
||||
if (updatedComposeData.services) {
|
||||
updatedComposeData.services = addPrefixToServiceNames(
|
||||
updatedComposeData.services,
|
||||
prefix,
|
||||
);
|
||||
}
|
||||
|
||||
return updatedComposeData;
|
||||
};
|
||||
78
server/utils/docker/compose/volume.ts
Normal file
78
server/utils/docker/compose/volume.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import _ from "lodash";
|
||||
import type {
|
||||
ComposeSpecification,
|
||||
DefinitionsService,
|
||||
DefinitionsVolume,
|
||||
} from "../types";
|
||||
|
||||
// Función para agregar prefijo a volúmenes
|
||||
export const addPrefixToVolumesRoot = (
|
||||
volumes: { [key: string]: DefinitionsVolume },
|
||||
prefix: string,
|
||||
): { [key: string]: DefinitionsVolume } => {
|
||||
return _.mapKeys(volumes, (_value, key) => `${key}-${prefix}`);
|
||||
};
|
||||
|
||||
export const addPrefixToVolumesInServices = (
|
||||
services: { [key: string]: DefinitionsService },
|
||||
prefix: string,
|
||||
): { [key: string]: DefinitionsService } => {
|
||||
const newServices: { [key: string]: DefinitionsService } = {};
|
||||
|
||||
_.forEach(services, (serviceConfig, serviceName) => {
|
||||
const newServiceConfig = _.cloneDeep(serviceConfig);
|
||||
|
||||
// Reemplazar nombres de volúmenes en volumes
|
||||
if (_.has(newServiceConfig, "volumes")) {
|
||||
newServiceConfig.volumes = _.map(newServiceConfig.volumes, (volume) => {
|
||||
if (_.isString(volume)) {
|
||||
const [volumeName, path] = volume.split(":");
|
||||
|
||||
// skip bind mounts and variables (e.g. $PWD)
|
||||
if (
|
||||
volumeName?.startsWith(".") ||
|
||||
volumeName?.startsWith("/") ||
|
||||
volumeName?.startsWith("$")
|
||||
) {
|
||||
return volume;
|
||||
}
|
||||
return `${volumeName}-${prefix}:${path}`;
|
||||
}
|
||||
if (_.isObject(volume) && volume.type === "volume" && volume.source) {
|
||||
return {
|
||||
...volume,
|
||||
source: `${volume.source}-${prefix}`,
|
||||
};
|
||||
}
|
||||
return volume;
|
||||
});
|
||||
}
|
||||
|
||||
newServices[serviceName] = newServiceConfig;
|
||||
});
|
||||
|
||||
return newServices;
|
||||
};
|
||||
|
||||
export const addPrefixToAllVolumes = (
|
||||
composeData: ComposeSpecification,
|
||||
prefix: string,
|
||||
): ComposeSpecification => {
|
||||
const updatedComposeData = { ...composeData };
|
||||
|
||||
if (updatedComposeData.volumes) {
|
||||
updatedComposeData.volumes = addPrefixToVolumesRoot(
|
||||
updatedComposeData.volumes,
|
||||
prefix,
|
||||
);
|
||||
}
|
||||
|
||||
if (updatedComposeData.services) {
|
||||
updatedComposeData.services = addPrefixToVolumesInServices(
|
||||
updatedComposeData.services,
|
||||
prefix,
|
||||
);
|
||||
}
|
||||
|
||||
return updatedComposeData;
|
||||
};
|
||||
879
server/utils/docker/types.ts
Normal file
879
server/utils/docker/types.ts
Normal file
@@ -0,0 +1,879 @@
|
||||
export type DefinitionsInclude =
|
||||
| string
|
||||
| {
|
||||
path?: StringOrList;
|
||||
env_file?: StringOrList;
|
||||
project_directory?: string;
|
||||
};
|
||||
export type StringOrList = string | ListOfStrings;
|
||||
export type ListOfStrings = string[];
|
||||
export type DefinitionsDevelopment = {
|
||||
watch?: {
|
||||
ignore?: string[];
|
||||
path: string;
|
||||
action: "rebuild" | "sync" | "sync+restart";
|
||||
target?: string;
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
[k: string]: unknown;
|
||||
} & Development;
|
||||
export type Development = {
|
||||
watch?: {
|
||||
ignore?: string[];
|
||||
path: string;
|
||||
action: "rebuild" | "sync" | "sync+restart";
|
||||
target?: string;
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
[k: string]: unknown;
|
||||
} | null;
|
||||
export type DefinitionsDeployment = {
|
||||
mode?: string;
|
||||
endpoint_mode?: string;
|
||||
replicas?: number;
|
||||
labels?: ListOrDict;
|
||||
rollback_config?: {
|
||||
parallelism?: number;
|
||||
delay?: string;
|
||||
failure_action?: string;
|
||||
monitor?: string;
|
||||
max_failure_ratio?: number;
|
||||
order?: "start-first" | "stop-first";
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
};
|
||||
update_config?: {
|
||||
parallelism?: number;
|
||||
delay?: string;
|
||||
failure_action?: string;
|
||||
monitor?: string;
|
||||
max_failure_ratio?: number;
|
||||
order?: "start-first" | "stop-first";
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
};
|
||||
resources?: {
|
||||
limits?: {
|
||||
cpus?: number | string;
|
||||
memory?: string;
|
||||
pids?: number;
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
};
|
||||
reservations?: {
|
||||
cpus?: number | string;
|
||||
memory?: string;
|
||||
generic_resources?: DefinitionsGenericResources;
|
||||
devices?: DefinitionsDevices;
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
};
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
};
|
||||
restart_policy?: {
|
||||
condition?: string;
|
||||
delay?: string;
|
||||
max_attempts?: number;
|
||||
window?: string;
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
};
|
||||
placement?: {
|
||||
constraints?: string[];
|
||||
preferences?: {
|
||||
spread?: string;
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
max_replicas_per_node?: number;
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
};
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
} & Deployment;
|
||||
export type ListOrDict =
|
||||
| {
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` ".+".
|
||||
*/
|
||||
[k: string]: string | number | boolean | null;
|
||||
}
|
||||
| string[];
|
||||
export type DefinitionsGenericResources = {
|
||||
discrete_resource_spec?: {
|
||||
kind?: string;
|
||||
value?: number;
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
};
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
export type DefinitionsDevices = {
|
||||
capabilities?: ListOfStrings;
|
||||
count?: string | number;
|
||||
device_ids?: ListOfStrings;
|
||||
driver?: string;
|
||||
options?: ListOrDict;
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
export type Deployment = {
|
||||
mode?: string;
|
||||
endpoint_mode?: string;
|
||||
replicas?: number;
|
||||
labels?: ListOrDict;
|
||||
rollback_config?: {
|
||||
parallelism?: number;
|
||||
delay?: string;
|
||||
failure_action?: string;
|
||||
monitor?: string;
|
||||
max_failure_ratio?: number;
|
||||
order?: "start-first" | "stop-first";
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
};
|
||||
update_config?: {
|
||||
parallelism?: number;
|
||||
delay?: string;
|
||||
failure_action?: string;
|
||||
monitor?: string;
|
||||
max_failure_ratio?: number;
|
||||
order?: "start-first" | "stop-first";
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
};
|
||||
resources?: {
|
||||
limits?: {
|
||||
cpus?: number | string;
|
||||
memory?: string;
|
||||
pids?: number;
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
};
|
||||
reservations?: {
|
||||
cpus?: number | string;
|
||||
memory?: string;
|
||||
generic_resources?: DefinitionsGenericResources;
|
||||
devices?: DefinitionsDevices;
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
};
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
};
|
||||
restart_policy?: {
|
||||
condition?: string;
|
||||
delay?: string;
|
||||
max_attempts?: number;
|
||||
window?: string;
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
};
|
||||
placement?: {
|
||||
constraints?: string[];
|
||||
preferences?: {
|
||||
spread?: string;
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
max_replicas_per_node?: number;
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
};
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
} | null;
|
||||
export type ServiceConfigOrSecret = (
|
||||
| string
|
||||
| {
|
||||
source?: string;
|
||||
target?: string;
|
||||
uid?: string;
|
||||
gid?: string;
|
||||
mode?: number;
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
}
|
||||
)[];
|
||||
export type Command = null | string | string[];
|
||||
export type EnvFile =
|
||||
| string
|
||||
| (
|
||||
| string
|
||||
| {
|
||||
path: string;
|
||||
required?: boolean;
|
||||
}
|
||||
)[];
|
||||
/**
|
||||
* This interface was referenced by `PropertiesNetworks`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^[a-zA-Z0-9._-]+$".
|
||||
*/
|
||||
export type DefinitionsNetwork = {
|
||||
name?: string;
|
||||
driver?: string;
|
||||
driver_opts?: {
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^.+$".
|
||||
*/
|
||||
[k: string]: string | number;
|
||||
};
|
||||
ipam?: {
|
||||
driver?: string;
|
||||
config?: {
|
||||
subnet?: string;
|
||||
ip_range?: string;
|
||||
gateway?: string;
|
||||
aux_addresses?: {
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^.+$".
|
||||
*/
|
||||
[k: string]: string;
|
||||
};
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
options?: {
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^.+$".
|
||||
*/
|
||||
[k: string]: string;
|
||||
};
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
};
|
||||
external?:
|
||||
| boolean
|
||||
| {
|
||||
name?: string;
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
};
|
||||
internal?: boolean;
|
||||
enable_ipv6?: boolean;
|
||||
attachable?: boolean;
|
||||
labels?: ListOrDict;
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
} & Network;
|
||||
export type Network = {
|
||||
name?: string;
|
||||
driver?: string;
|
||||
driver_opts?: {
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^.+$".
|
||||
*/
|
||||
[k: string]: string | number;
|
||||
};
|
||||
ipam?: {
|
||||
driver?: string;
|
||||
config?: {
|
||||
subnet?: string;
|
||||
ip_range?: string;
|
||||
gateway?: string;
|
||||
aux_addresses?: {
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^.+$".
|
||||
*/
|
||||
[k: string]: string;
|
||||
};
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
options?: {
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^.+$".
|
||||
*/
|
||||
[k: string]: string;
|
||||
};
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
};
|
||||
external?:
|
||||
| boolean
|
||||
| {
|
||||
name?: string;
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
};
|
||||
internal?: boolean;
|
||||
enable_ipv6?: boolean;
|
||||
attachable?: boolean;
|
||||
labels?: ListOrDict;
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
} | null;
|
||||
/**
|
||||
* This interface was referenced by `PropertiesVolumes`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^[a-zA-Z0-9._-]+$".
|
||||
*/
|
||||
export type DefinitionsVolume = {
|
||||
name?: string;
|
||||
driver?: string;
|
||||
driver_opts?: {
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^.+$".
|
||||
*/
|
||||
[k: string]: string | number;
|
||||
};
|
||||
external?:
|
||||
| boolean
|
||||
| {
|
||||
name?: string;
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
};
|
||||
labels?: ListOrDict;
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
} & Volume;
|
||||
export type Volume = {
|
||||
name?: string;
|
||||
driver?: string;
|
||||
driver_opts?: {
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^.+$".
|
||||
*/
|
||||
[k: string]: string | number;
|
||||
};
|
||||
external?:
|
||||
| boolean
|
||||
| {
|
||||
name?: string;
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
};
|
||||
labels?: ListOrDict;
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
} | null;
|
||||
|
||||
/**
|
||||
* The Compose file is a YAML file defining a multi-containers based application.
|
||||
*/
|
||||
export interface ComposeSpecification {
|
||||
/**
|
||||
* declared for backward compatibility, ignored.
|
||||
*/
|
||||
version?: string;
|
||||
/**
|
||||
* define the Compose project name, until user defines one explicitly.
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* compose sub-projects to be included.
|
||||
*/
|
||||
include?: DefinitionsInclude[];
|
||||
services?: PropertiesServices;
|
||||
networks?: PropertiesNetworks;
|
||||
volumes?: PropertiesVolumes;
|
||||
secrets?: PropertiesSecrets;
|
||||
configs?: PropertiesConfigs;
|
||||
/**
|
||||
* This interface was referenced by `ComposeSpecification`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface PropertiesServices {
|
||||
[k: string]: DefinitionsService;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `PropertiesServices`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^[a-zA-Z0-9._-]+$".
|
||||
*/
|
||||
export interface DefinitionsService {
|
||||
develop?: DefinitionsDevelopment;
|
||||
deploy?: DefinitionsDeployment;
|
||||
annotations?: ListOrDict;
|
||||
attach?: boolean;
|
||||
build?:
|
||||
| string
|
||||
| {
|
||||
context?: string;
|
||||
dockerfile?: string;
|
||||
dockerfile_inline?: string;
|
||||
entitlements?: string[];
|
||||
args?: ListOrDict;
|
||||
ssh?: ListOrDict;
|
||||
labels?: ListOrDict;
|
||||
cache_from?: string[];
|
||||
cache_to?: string[];
|
||||
no_cache?: boolean;
|
||||
additional_contexts?: ListOrDict;
|
||||
network?: string;
|
||||
pull?: boolean;
|
||||
target?: string;
|
||||
shm_size?: number | string;
|
||||
extra_hosts?: ListOrDict;
|
||||
isolation?: string;
|
||||
privileged?: boolean;
|
||||
secrets?: ServiceConfigOrSecret;
|
||||
tags?: string[];
|
||||
ulimits?: Ulimits;
|
||||
platforms?: string[];
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
};
|
||||
blkio_config?: {
|
||||
device_read_bps?: BlkioLimit[];
|
||||
device_read_iops?: BlkioLimit[];
|
||||
device_write_bps?: BlkioLimit[];
|
||||
device_write_iops?: BlkioLimit[];
|
||||
weight?: number;
|
||||
weight_device?: BlkioWeight[];
|
||||
};
|
||||
cap_add?: string[];
|
||||
cap_drop?: string[];
|
||||
cgroup?: "host" | "private";
|
||||
cgroup_parent?: string;
|
||||
command?: Command;
|
||||
configs?: ServiceConfigOrSecret;
|
||||
container_name?: string;
|
||||
cpu_count?: number;
|
||||
cpu_percent?: number;
|
||||
cpu_shares?: number | string;
|
||||
cpu_quota?: number | string;
|
||||
cpu_period?: number | string;
|
||||
cpu_rt_period?: number | string;
|
||||
cpu_rt_runtime?: number | string;
|
||||
cpus?: number | string;
|
||||
cpuset?: string;
|
||||
credential_spec?: {
|
||||
config?: string;
|
||||
file?: string;
|
||||
registry?: string;
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
};
|
||||
depends_on?:
|
||||
| ListOfStrings
|
||||
| {
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^[a-zA-Z0-9._-]+$".
|
||||
*/
|
||||
[k: string]: {
|
||||
restart?: boolean;
|
||||
required?: boolean;
|
||||
condition:
|
||||
| "service_started"
|
||||
| "service_healthy"
|
||||
| "service_completed_successfully";
|
||||
};
|
||||
};
|
||||
device_cgroup_rules?: ListOfStrings;
|
||||
devices?: string[];
|
||||
dns?: StringOrList;
|
||||
dns_opt?: string[];
|
||||
dns_search?: StringOrList;
|
||||
domainname?: string;
|
||||
entrypoint?: Command;
|
||||
env_file?: EnvFile;
|
||||
environment?: ListOrDict;
|
||||
expose?: (string | number)[];
|
||||
extends?:
|
||||
| string
|
||||
| {
|
||||
service: string;
|
||||
file?: string;
|
||||
};
|
||||
external_links?: string[];
|
||||
extra_hosts?: ListOrDict;
|
||||
group_add?: (string | number)[];
|
||||
healthcheck?: DefinitionsHealthcheck;
|
||||
hostname?: string;
|
||||
image?: string;
|
||||
init?: boolean;
|
||||
ipc?: string;
|
||||
isolation?: string;
|
||||
labels?: ListOrDict;
|
||||
links?: string[];
|
||||
logging?: {
|
||||
driver?: string;
|
||||
options?: {
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^.+$".
|
||||
*/
|
||||
[k: string]: string | number | null;
|
||||
};
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
};
|
||||
mac_address?: string;
|
||||
mem_limit?: number | string;
|
||||
mem_reservation?: string | number;
|
||||
mem_swappiness?: number;
|
||||
memswap_limit?: number | string;
|
||||
network_mode?: string;
|
||||
networks?:
|
||||
| ListOfStrings
|
||||
| {
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^[a-zA-Z0-9._-]+$".
|
||||
*/
|
||||
[k: string]: {
|
||||
aliases?: ListOfStrings;
|
||||
ipv4_address?: string;
|
||||
ipv6_address?: string;
|
||||
link_local_ips?: ListOfStrings;
|
||||
mac_address?: string;
|
||||
driver_opts?: {
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^.+$".
|
||||
*/
|
||||
[k: string]: string | number;
|
||||
};
|
||||
priority?: number;
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
} | null;
|
||||
};
|
||||
oom_kill_disable?: boolean;
|
||||
oom_score_adj?: number;
|
||||
pid?: string | null;
|
||||
pids_limit?: number | string;
|
||||
platform?: string;
|
||||
ports?: (
|
||||
| number
|
||||
| string
|
||||
| {
|
||||
name?: string;
|
||||
mode?: string;
|
||||
host_ip?: string;
|
||||
target?: number;
|
||||
published?: string | number;
|
||||
protocol?: string;
|
||||
app_protocol?: string;
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
}
|
||||
)[];
|
||||
privileged?: boolean;
|
||||
profiles?: ListOfStrings;
|
||||
pull_policy?: "always" | "never" | "if_not_present" | "build" | "missing";
|
||||
read_only?: boolean;
|
||||
restart?: string;
|
||||
runtime?: string;
|
||||
scale?: number;
|
||||
security_opt?: string[];
|
||||
shm_size?: number | string;
|
||||
secrets?: ServiceConfigOrSecret;
|
||||
sysctls?: ListOrDict;
|
||||
stdin_open?: boolean;
|
||||
stop_grace_period?: string;
|
||||
stop_signal?: string;
|
||||
storage_opt?: {
|
||||
[k: string]: unknown;
|
||||
};
|
||||
tmpfs?: StringOrList;
|
||||
tty?: boolean;
|
||||
ulimits?: Ulimits;
|
||||
user?: string;
|
||||
uts?: string;
|
||||
userns_mode?: string;
|
||||
volumes?: (
|
||||
| string
|
||||
| {
|
||||
type: string;
|
||||
source?: string;
|
||||
target?: string;
|
||||
read_only?: boolean;
|
||||
consistency?: string;
|
||||
bind?: {
|
||||
propagation?: string;
|
||||
create_host_path?: boolean;
|
||||
selinux?: "z" | "Z";
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
};
|
||||
volume?: {
|
||||
nocopy?: boolean;
|
||||
subpath?: string;
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
};
|
||||
tmpfs?: {
|
||||
size?: number | string;
|
||||
mode?: number;
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
};
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
}
|
||||
)[];
|
||||
volumes_from?: string[];
|
||||
working_dir?: string;
|
||||
/**
|
||||
* This interface was referenced by `DefinitionsService`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface Ulimits {
|
||||
/**
|
||||
* This interface was referenced by `Ulimits`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^[a-z]+$".
|
||||
*/
|
||||
[k: string]:
|
||||
| number
|
||||
| {
|
||||
hard: number;
|
||||
soft: number;
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
};
|
||||
}
|
||||
export interface BlkioLimit {
|
||||
path?: string;
|
||||
rate?: number | string;
|
||||
}
|
||||
export interface BlkioWeight {
|
||||
path?: string;
|
||||
weight?: number;
|
||||
}
|
||||
export interface DefinitionsHealthcheck {
|
||||
disable?: boolean;
|
||||
interval?: string;
|
||||
retries?: number;
|
||||
test?: string | string[];
|
||||
timeout?: string;
|
||||
start_period?: string;
|
||||
start_interval?: string;
|
||||
/**
|
||||
* This interface was referenced by `DefinitionsHealthcheck`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface PropertiesNetworks {
|
||||
[k: string]: DefinitionsNetwork;
|
||||
}
|
||||
export interface PropertiesVolumes {
|
||||
[k: string]: DefinitionsVolume;
|
||||
}
|
||||
export interface PropertiesSecrets {
|
||||
[k: string]: DefinitionsSecret;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `PropertiesSecrets`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^[a-zA-Z0-9._-]+$".
|
||||
*/
|
||||
export interface DefinitionsSecret {
|
||||
name?: string;
|
||||
environment?: string;
|
||||
file?: string;
|
||||
external?:
|
||||
| boolean
|
||||
| {
|
||||
name?: string;
|
||||
[k: string]: unknown;
|
||||
};
|
||||
labels?: ListOrDict;
|
||||
driver?: string;
|
||||
driver_opts?: {
|
||||
/**
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^.+$".
|
||||
*/
|
||||
[k: string]: string | number;
|
||||
};
|
||||
template_driver?: string;
|
||||
/**
|
||||
* This interface was referenced by `DefinitionsSecret`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface PropertiesConfigs {
|
||||
[k: string]: DefinitionsConfig;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `PropertiesConfigs`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^[a-zA-Z0-9._-]+$".
|
||||
*/
|
||||
export interface DefinitionsConfig {
|
||||
name?: string;
|
||||
content?: string;
|
||||
environment?: string;
|
||||
file?: string;
|
||||
external?:
|
||||
| boolean
|
||||
| {
|
||||
name?: string;
|
||||
[k: string]: unknown;
|
||||
};
|
||||
labels?: ListOrDict;
|
||||
template_driver?: string;
|
||||
/**
|
||||
* This interface was referenced by `DefinitionsConfig`'s JSON-Schema definition
|
||||
* via the `patternProperty` "^x-".
|
||||
*/
|
||||
[k: string]: unknown;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { Readable } from "node:stream";
|
||||
import { APPLICATIONS_PATH, docker } from "@/server/constants";
|
||||
import { APPLICATIONS_PATH, COMPOSE_PATH, docker } from "@/server/constants";
|
||||
import type { ContainerInfo, ResourceRequirements } from "dockerode";
|
||||
import type { ApplicationNested } from "../builders";
|
||||
import { execAsync } from "../process/execAsync";
|
||||
@@ -324,6 +324,33 @@ export const generateFileMounts = (
|
||||
});
|
||||
};
|
||||
|
||||
export const generateFileMountsCompose = (
|
||||
appName: string,
|
||||
mounts: ApplicationNested["mounts"],
|
||||
) => {
|
||||
if (!mounts || mounts.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return mounts
|
||||
.filter((mount) => mount.type === "file")
|
||||
.map((mount) => {
|
||||
const fileName = path.basename(mount.mountPath);
|
||||
const directory = path.join(
|
||||
COMPOSE_PATH,
|
||||
appName,
|
||||
path.dirname(mount.mountPath),
|
||||
);
|
||||
fs.mkdirSync(directory, { recursive: true });
|
||||
|
||||
const filePath = path.join(directory, fileName);
|
||||
|
||||
fs.writeFileSync(filePath, mount.content || "");
|
||||
|
||||
return {};
|
||||
});
|
||||
};
|
||||
|
||||
export const getServiceContainer = async (appName: string) => {
|
||||
try {
|
||||
const filter = {
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import fs, { promises as fsPromises } from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { Application } from "@/server/api/services/application";
|
||||
import { APPLICATIONS_PATH, MONITORING_PATH } from "@/server/constants";
|
||||
import {
|
||||
APPLICATIONS_PATH,
|
||||
COMPOSE_PATH,
|
||||
MONITORING_PATH,
|
||||
} from "@/server/constants";
|
||||
import { execAsync } from "../process/execAsync";
|
||||
|
||||
export const recreateDirectory = async (pathFolder: string): Promise<void> => {
|
||||
@@ -32,6 +36,16 @@ export const removeDirectoryCode = async (appName: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const removeComposeDirectory = async (appName: string) => {
|
||||
const directoryPath = path.join(COMPOSE_PATH, appName);
|
||||
try {
|
||||
await execAsync(`rm -rf ${directoryPath}`);
|
||||
} catch (error) {
|
||||
console.error(`Error to remove ${directoryPath}: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const removeMonitoringDirectory = async (appName: string) => {
|
||||
const directoryPath = path.join(MONITORING_PATH, appName);
|
||||
try {
|
||||
|
||||
@@ -1,58 +1,58 @@
|
||||
import {
|
||||
type ChildProcess,
|
||||
type SpawnOptions,
|
||||
spawn,
|
||||
type ChildProcess,
|
||||
type SpawnOptions,
|
||||
spawn,
|
||||
} from "node:child_process";
|
||||
import BufferList from "bl";
|
||||
|
||||
export const spawnAsync = (
|
||||
command: string,
|
||||
args?: string[] | undefined,
|
||||
onData?: (data: string) => void, // Callback opcional para manejar datos en tiempo real
|
||||
options?: SpawnOptions,
|
||||
command: string,
|
||||
args?: string[] | undefined,
|
||||
onData?: (data: string) => void, // Callback opcional para manejar datos en tiempo real
|
||||
options?: SpawnOptions,
|
||||
): Promise<BufferList> & { child: ChildProcess } => {
|
||||
const child = spawn(command, args ?? [], options ?? {});
|
||||
const stdout = child.stdout ? new BufferList() : new BufferList();
|
||||
const stderr = child.stderr ? new BufferList() : new BufferList();
|
||||
const child = spawn(command, args ?? [], options ?? {});
|
||||
const stdout = child.stdout ? new BufferList() : new BufferList();
|
||||
const stderr = child.stderr ? new BufferList() : new BufferList();
|
||||
|
||||
if (child.stdout) {
|
||||
child.stdout.on("data", (data) => {
|
||||
stdout.append(data);
|
||||
if (onData) {
|
||||
onData(data.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
if (child.stderr) {
|
||||
child.stderr.on("data", (data) => {
|
||||
stderr.append(data);
|
||||
if (onData) {
|
||||
onData(data.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
if (child.stdout) {
|
||||
child.stdout.on("data", (data) => {
|
||||
stdout.append(data);
|
||||
if (onData) {
|
||||
onData(data.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
if (child.stderr) {
|
||||
child.stderr.on("data", (data) => {
|
||||
stderr.append(data);
|
||||
if (onData) {
|
||||
onData(data.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const promise = new Promise<BufferList>((resolve, reject) => {
|
||||
child.on("error", reject);
|
||||
const promise = new Promise<BufferList>((resolve, reject) => {
|
||||
child.on("error", reject);
|
||||
|
||||
child.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
resolve(stdout);
|
||||
} else {
|
||||
const err = new Error(`child exited with code ${code}`) as Error & {
|
||||
code: number;
|
||||
stderr: BufferList;
|
||||
stdout: BufferList;
|
||||
};
|
||||
err.code = code || -1;
|
||||
err.stderr = stderr;
|
||||
err.stdout = stdout;
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
}) as Promise<BufferList> & { child: ChildProcess };
|
||||
child.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
resolve(stdout);
|
||||
} else {
|
||||
const err = new Error(`child exited with code ${code}`) as Error & {
|
||||
code: number;
|
||||
stderr: BufferList;
|
||||
stdout: BufferList;
|
||||
};
|
||||
err.code = code || -1;
|
||||
err.stderr = stderr;
|
||||
err.stdout = stdout;
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
}) as Promise<BufferList> & { child: ChildProcess };
|
||||
|
||||
promise.child = child;
|
||||
promise.child = child;
|
||||
|
||||
return promise;
|
||||
return promise;
|
||||
};
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { Application } from "@/server/api/services/application";
|
||||
import { APPLICATIONS_PATH, SSH_PATH } from "@/server/constants";
|
||||
import path, { join } from "node:path";
|
||||
import { APPLICATIONS_PATH, COMPOSE_PATH, SSH_PATH } from "@/server/constants";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { recreateDirectory } from "../filesystem/directory";
|
||||
import { execAsync } from "../process/execAsync";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
|
||||
export const cloneGitRepository = async (
|
||||
application: Application,
|
||||
entity: {
|
||||
appName: string;
|
||||
customGitUrl?: string | null;
|
||||
customGitBranch?: string | null;
|
||||
customGitSSHKey?: string | null;
|
||||
},
|
||||
logPath: string,
|
||||
isCompose = false,
|
||||
) => {
|
||||
const { appName, customGitUrl, customGitBranch, customGitSSHKey } =
|
||||
application;
|
||||
const { appName, customGitUrl, customGitBranch, customGitSSHKey } = entity;
|
||||
|
||||
if (!customGitUrl || !customGitBranch) {
|
||||
throw new TRPCError({
|
||||
@@ -23,7 +27,8 @@ export const cloneGitRepository = async (
|
||||
|
||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||
const keyPath = path.join(SSH_PATH, `${appName}_rsa`);
|
||||
const outputPath = path.join(APPLICATIONS_PATH, appName);
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName);
|
||||
const knownHostsPath = path.join(SSH_PATH, "known_hosts");
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { Application } from "@/server/api/services/application";
|
||||
import { APPLICATIONS_PATH } from "@/server/constants";
|
||||
import { join } from "node:path";
|
||||
import { APPLICATIONS_PATH, COMPOSE_PATH } from "@/server/constants";
|
||||
import { createAppAuth } from "@octokit/auth-app";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { Octokit } from "octokit";
|
||||
@@ -49,25 +48,36 @@ export const haveGithubRequirements = (admin: Admin) => {
|
||||
);
|
||||
};
|
||||
|
||||
const getErrorCloneRequirements = (application: Application) => {
|
||||
const getErrorCloneRequirements = (entity: {
|
||||
repository?: string | null;
|
||||
owner?: string | null;
|
||||
branch?: string | null;
|
||||
}) => {
|
||||
const reasons: string[] = [];
|
||||
const { repository, owner, branch } = application;
|
||||
const { repository, owner, branch } = entity;
|
||||
|
||||
if (!repository) reasons.push("1. Repository not assigned.");
|
||||
if (!owner) reasons.push("2. Owner not specified .");
|
||||
if (!owner) reasons.push("2. Owner not specified.");
|
||||
if (!branch) reasons.push("3. Branch not defined.");
|
||||
|
||||
return reasons;
|
||||
};
|
||||
|
||||
export const cloneGithubRepository = async (
|
||||
admin: Admin,
|
||||
application: Application,
|
||||
entity: {
|
||||
appName: string;
|
||||
repository?: string | null;
|
||||
owner?: string | null;
|
||||
branch?: string | null;
|
||||
},
|
||||
logPath: string,
|
||||
isCompose = false,
|
||||
) => {
|
||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||
const { appName, repository, owner, branch } = application;
|
||||
const { appName, repository, owner, branch } = entity;
|
||||
|
||||
const requirements = getErrorCloneRequirements(application);
|
||||
const requirements = getErrorCloneRequirements(entity);
|
||||
|
||||
// Check if requirements are met
|
||||
if (requirements.length > 0) {
|
||||
@@ -82,7 +92,8 @@ export const cloneGithubRepository = async (
|
||||
message: "Error: GitHub repository information is incomplete.",
|
||||
});
|
||||
}
|
||||
const outputPath = path.join(APPLICATIONS_PATH, appName);
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName);
|
||||
const octokit = authGithub(admin);
|
||||
const token = await getGithubToken(octokit);
|
||||
const repoclone = `github.com/${owner}/${repository}.git`;
|
||||
|
||||
28
server/utils/providers/raw.ts
Normal file
28
server/utils/providers/raw.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { Compose } from "@/server/api/services/compose";
|
||||
import { COMPOSE_PATH } from "@/server/constants";
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { recreateDirectory } from "../filesystem/directory";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
|
||||
export const createComposeFile = async (compose: Compose, logPath: string) => {
|
||||
const { appName, composeFile } = compose;
|
||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||
const outputPath = join(COMPOSE_PATH, appName);
|
||||
|
||||
try {
|
||||
await recreateDirectory(outputPath);
|
||||
writeStream.write(
|
||||
`\nCreating File 'docker-compose.yml' to ${outputPath}: ✅\n`,
|
||||
);
|
||||
|
||||
await writeFile(join(outputPath, "docker-compose.yml"), composeFile);
|
||||
|
||||
writeStream.write(`\nFile 'docker-compose.yml' created: ✅\n`);
|
||||
} catch (error) {
|
||||
writeStream.write(`\nERROR Creating Compose File: ${error}: ❌\n`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
}
|
||||
};
|
||||
@@ -31,6 +31,10 @@ export const setupDockerStatsMonitoringSocketServer = (
|
||||
wssTerm.on("connection", async (ws, req) => {
|
||||
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
||||
const appName = url.searchParams.get("appName");
|
||||
const appType = (url.searchParams.get("appType") || "application") as
|
||||
| "application"
|
||||
| "stack"
|
||||
| "docker-compose";
|
||||
const { user, session } = await validateWebSocketRequest(req);
|
||||
|
||||
if (!appName) {
|
||||
@@ -47,7 +51,15 @@ export const setupDockerStatsMonitoringSocketServer = (
|
||||
try {
|
||||
const filter = {
|
||||
status: ["running"],
|
||||
label: [`com.docker.swarm.service.name=${appName}`],
|
||||
...(appType === "application" && {
|
||||
label: [`com.docker.swarm.service.name=${appName}`],
|
||||
}),
|
||||
...(appType === "stack" && {
|
||||
label: [`com.docker.swarm.task.name=${appName}`],
|
||||
}),
|
||||
...(appType === "docker-compose" && {
|
||||
name: [appName],
|
||||
}),
|
||||
};
|
||||
|
||||
const containers = await docker.listContainers({
|
||||
|
||||
Reference in New Issue
Block a user