mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Merge branch 'canary' into hehehai/feat-server-custom-name
This commit is contained in:
@@ -20,6 +20,9 @@ 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";
|
||||
/**
|
||||
* This is the primary router for your server.
|
||||
*
|
||||
@@ -47,6 +50,9 @@ export const appRouter = createTRPCRouter({
|
||||
security: securityRouter,
|
||||
redirects: redirectsRouter,
|
||||
port: portRouter,
|
||||
compose: composeRouter,
|
||||
registry: registryRouter,
|
||||
cluster: clusterRouter,
|
||||
});
|
||||
|
||||
// export type definition of API
|
||||
|
||||
@@ -152,11 +152,18 @@ export const adminRouter = createTRPCRouter({
|
||||
installationId: admin.githubInstallationId,
|
||||
},
|
||||
});
|
||||
const branches = await octokit.rest.repos.listBranches({
|
||||
owner: input.owner,
|
||||
repo: input.repo,
|
||||
});
|
||||
return branches.data;
|
||||
|
||||
const branches = (await octokit.paginate(
|
||||
octokit.rest.repos.listBranches,
|
||||
{
|
||||
owner: input.owner,
|
||||
repo: input.repo,
|
||||
},
|
||||
)) as unknown as Awaited<
|
||||
ReturnType<typeof octokit.rest.repos.listBranches>
|
||||
>["data"];
|
||||
|
||||
return branches;
|
||||
}),
|
||||
haveGithubConfigured: protectedProcedure.query(async () => {
|
||||
const adminResponse = await findAdmin();
|
||||
|
||||
@@ -163,6 +163,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
applicationId: input.applicationId,
|
||||
titleLog: "Rebuild deployment",
|
||||
type: "redeploy",
|
||||
applicationType: "application",
|
||||
};
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
@@ -294,6 +295,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
applicationId: input.applicationId,
|
||||
titleLog: "Manual deployment",
|
||||
type: "deploy",
|
||||
applicationType: "application",
|
||||
};
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
|
||||
48
server/api/routers/cluster.ts
Normal file
48
server/api/routers/cluster.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { docker } from "@/server/constants";
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
import { getPublicIpWithFallback } from "@/server/wss/terminal";
|
||||
import type { DockerNode } from "../services/cluster";
|
||||
import { z } from "zod";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { execAsync } from "@/server/utils/process/execAsync";
|
||||
|
||||
export const clusterRouter = createTRPCRouter({
|
||||
getNodes: protectedProcedure.query(async () => {
|
||||
const workers: DockerNode[] = await docker.listNodes();
|
||||
|
||||
return workers;
|
||||
}),
|
||||
removeWorker: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
nodeId: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
await execAsync(
|
||||
`docker node update --availability drain ${input.nodeId}`,
|
||||
);
|
||||
await execAsync(`docker node rm ${input.nodeId} --force`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "Error to remove the node",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
addWorker: protectedProcedure.query(async ({ input }) => {
|
||||
const result = await docker.swarmInspect();
|
||||
return `docker swarm join --token ${
|
||||
result.JoinTokens.Worker
|
||||
} ${await getPublicIpWithFallback()}:2377`;
|
||||
}),
|
||||
addManager: protectedProcedure.query(async ({ input }) => {
|
||||
const result = await docker.swarmInspect();
|
||||
return `docker swarm join --token ${
|
||||
result.JoinTokens.Manager
|
||||
} ${await getPublicIpWithFallback()}:2377`;
|
||||
}),
|
||||
});
|
||||
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),
|
||||
});
|
||||
|
||||
88
server/api/routers/registry.ts
Normal file
88
server/api/routers/registry.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import {
|
||||
apiCreateRegistry,
|
||||
apiEnableSelfHostedRegistry,
|
||||
apiFindOneRegistry,
|
||||
apiRemoveRegistry,
|
||||
apiTestRegistry,
|
||||
apiUpdateRegistry,
|
||||
} from "@/server/db/schema";
|
||||
import {
|
||||
createRegistry,
|
||||
findAllRegistry,
|
||||
findRegistryById,
|
||||
removeRegistry,
|
||||
updateRegistry,
|
||||
} from "../services/registry";
|
||||
import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { manageRegistry } from "@/server/utils/traefik/registry";
|
||||
import { initializeRegistry } from "@/server/setup/registry-setup";
|
||||
import { execAsync } from "@/server/utils/process/execAsync";
|
||||
|
||||
export const registryRouter = createTRPCRouter({
|
||||
create: adminProcedure
|
||||
.input(apiCreateRegistry)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
return await createRegistry(input);
|
||||
}),
|
||||
remove: adminProcedure
|
||||
.input(apiRemoveRegistry)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
return await removeRegistry(input.registryId);
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.input(apiUpdateRegistry)
|
||||
.mutation(async ({ input }) => {
|
||||
const { registryId, ...rest } = input;
|
||||
const application = await updateRegistry(registryId, {
|
||||
...rest,
|
||||
});
|
||||
|
||||
if (!application) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Update: Error to update registry",
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}),
|
||||
all: protectedProcedure.query(async () => {
|
||||
return await findAllRegistry();
|
||||
}),
|
||||
one: adminProcedure.input(apiFindOneRegistry).query(async ({ input }) => {
|
||||
return await findRegistryById(input.registryId);
|
||||
}),
|
||||
testRegistry: protectedProcedure
|
||||
.input(apiTestRegistry)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`;
|
||||
await execAsync(loginCommand);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log("Error Registry:", error);
|
||||
return false;
|
||||
}
|
||||
}),
|
||||
|
||||
enableSelfHostedRegistry: adminProcedure
|
||||
.input(apiEnableSelfHostedRegistry)
|
||||
.mutation(async ({ input }) => {
|
||||
const selfHostedRegistry = await createRegistry({
|
||||
...input,
|
||||
registryName: "Self Hosted Registry",
|
||||
registryType: "selfHosted",
|
||||
registryUrl:
|
||||
process.env.NODE_ENV === "production"
|
||||
? input.registryUrl
|
||||
: "dokploy-registry.docker.localhost",
|
||||
imagePrefix: null,
|
||||
});
|
||||
|
||||
await manageRegistry(selfHostedRegistry);
|
||||
await initializeRegistry(input.username, input.password);
|
||||
|
||||
return selfHostedRegistry;
|
||||
}),
|
||||
});
|
||||
@@ -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 }) => {
|
||||
@@ -181,7 +186,7 @@ export const settingsRouter = createTRPCRouter({
|
||||
return true;
|
||||
}),
|
||||
|
||||
checkAndUpdateImage: adminProcedure.query(async () => {
|
||||
checkAndUpdateImage: adminProcedure.mutation(async () => {
|
||||
return await pullLatestRelease();
|
||||
}),
|
||||
updateServer: adminProcedure.mutation(async () => {
|
||||
@@ -238,3 +243,4 @@ export const settingsRouter = createTRPCRouter({
|
||||
return readConfigInPath(input.path);
|
||||
}),
|
||||
});
|
||||
// apt-get install apache2-utils
|
||||
|
||||
@@ -73,6 +73,7 @@ export const findApplicationById = async (applicationId: string) => {
|
||||
redirects: true,
|
||||
security: true,
|
||||
ports: true,
|
||||
registry: true,
|
||||
},
|
||||
});
|
||||
if (!application) {
|
||||
|
||||
41
server/api/services/cluster.ts
Normal file
41
server/api/services/cluster.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
export interface DockerNode {
|
||||
ID: string;
|
||||
Version: {
|
||||
Index: number;
|
||||
};
|
||||
CreatedAt: string;
|
||||
UpdatedAt: string;
|
||||
Spec: {
|
||||
Name: string;
|
||||
Labels: Record<string, string>;
|
||||
Role: "worker" | "manager";
|
||||
Availability: "active" | "pause" | "drain";
|
||||
};
|
||||
Description: {
|
||||
Hostname: string;
|
||||
Platform: {
|
||||
Architecture: string;
|
||||
OS: string;
|
||||
};
|
||||
Resources: {
|
||||
NanoCPUs: number;
|
||||
MemoryBytes: number;
|
||||
};
|
||||
Engine: {
|
||||
EngineVersion: string;
|
||||
Plugins: Array<{
|
||||
Type: string;
|
||||
Name: string;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
Status: {
|
||||
State: "unknown" | "down" | "ready" | "disconnected";
|
||||
Message: string;
|
||||
Addr: string;
|
||||
};
|
||||
ManagerStatus?: {
|
||||
Leader: boolean;
|
||||
Addr: string;
|
||||
};
|
||||
}
|
||||
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();
|
||||
|
||||
@@ -46,6 +46,7 @@ export const findProjectById = async (projectId: string) => {
|
||||
mysql: true,
|
||||
postgres: true,
|
||||
redis: true,
|
||||
compose: true,
|
||||
},
|
||||
});
|
||||
if (!project) {
|
||||
@@ -120,3 +121,12 @@ export const validUniqueServerAppName = async (appName: string) => {
|
||||
|
||||
return nonEmptyProjects.length === 0;
|
||||
};
|
||||
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, "");
|
||||
};
|
||||
|
||||
122
server/api/services/registry.ts
Normal file
122
server/api/services/registry.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { type apiCreateRegistry, registry } from "@/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { db } from "@/server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { findAdmin } from "./admin";
|
||||
import {
|
||||
manageRegistry,
|
||||
removeSelfHostedRegistry,
|
||||
} from "@/server/utils/traefik/registry";
|
||||
import { removeService } from "@/server/utils/docker/utils";
|
||||
import { initializeRegistry } from "@/server/setup/registry-setup";
|
||||
import { execAsync } from "@/server/utils/process/execAsync";
|
||||
|
||||
export type Registry = typeof registry.$inferSelect;
|
||||
|
||||
export const createRegistry = async (input: typeof apiCreateRegistry._type) => {
|
||||
const admin = await findAdmin();
|
||||
|
||||
return await db.transaction(async (tx) => {
|
||||
const newRegistry = await tx
|
||||
.insert(registry)
|
||||
.values({
|
||||
...input,
|
||||
adminId: admin.adminId,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newRegistry) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting registry",
|
||||
});
|
||||
}
|
||||
|
||||
const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`;
|
||||
await execAsync(loginCommand);
|
||||
|
||||
return newRegistry;
|
||||
});
|
||||
};
|
||||
|
||||
export const removeRegistry = async (registryId: string) => {
|
||||
try {
|
||||
const response = await db
|
||||
.delete(registry)
|
||||
.where(eq(registry.registryId, registryId))
|
||||
.returning()
|
||||
.then((res) => res[0]);
|
||||
|
||||
if (!response) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Registry not found",
|
||||
});
|
||||
}
|
||||
|
||||
if (response.registryType === "selfHosted") {
|
||||
await removeSelfHostedRegistry();
|
||||
await removeService("dokploy-registry");
|
||||
}
|
||||
|
||||
await execAsync(`docker logout ${response.registryUrl}`);
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to remove this registry",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const updateRegistry = async (
|
||||
registryId: string,
|
||||
registryData: Partial<Registry>,
|
||||
) => {
|
||||
try {
|
||||
const response = await db
|
||||
.update(registry)
|
||||
.set({
|
||||
...registryData,
|
||||
})
|
||||
.where(eq(registry.registryId, registryId))
|
||||
.returning()
|
||||
.then((res) => res[0]);
|
||||
|
||||
if (response?.registryType === "selfHosted") {
|
||||
await manageRegistry(response);
|
||||
await initializeRegistry(response.username, response.password);
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to update this registry",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const findRegistryById = async (registryId: string) => {
|
||||
const registryResponse = await db.query.registry.findFirst({
|
||||
where: eq(registry.registryId, registryId),
|
||||
columns: {
|
||||
password: false,
|
||||
},
|
||||
});
|
||||
if (!registryResponse) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Registry not found",
|
||||
});
|
||||
}
|
||||
return registryResponse;
|
||||
};
|
||||
|
||||
export const findAllRegistry = async () => {
|
||||
const registryResponse = await db.query.registry.findMany();
|
||||
return registryResponse;
|
||||
};
|
||||
@@ -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,7 +9,9 @@ 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`;
|
||||
export const MONITORING_PATH = `${BASE_PATH}/monitoring`;
|
||||
export const docker = new Docker();
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import type { Config } from "drizzle-kit";
|
||||
import { defineConfig } from "drizzle-kit";
|
||||
|
||||
console.log("> Generating PG Schema:", process.env.DATABASE_URL);
|
||||
export default {
|
||||
export default defineConfig({
|
||||
schema: "./server/db/schema/index.ts",
|
||||
driver: "pg",
|
||||
dialect: "postgresql",
|
||||
dbCredentials: {
|
||||
connectionString: process.env.DATABASE_URL || "",
|
||||
url: process.env.DATABASE_URL || "",
|
||||
},
|
||||
verbose: true,
|
||||
strict: true,
|
||||
out: "drizzle",
|
||||
} satisfies Config;
|
||||
migrations: {
|
||||
table: "migrations",
|
||||
schema: "public",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import { users } from "./user";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { z } from "zod";
|
||||
import { certificateType } from "./shared";
|
||||
import { registry } from "./registry";
|
||||
|
||||
export const admins = pgTable("admin", {
|
||||
adminId: text("adminId")
|
||||
@@ -39,6 +40,7 @@ export const adminsRelations = relations(admins, ({ one, many }) => ({
|
||||
references: [auth.id],
|
||||
}),
|
||||
users: many(users),
|
||||
registry: many(registry),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(admins, {
|
||||
|
||||
@@ -10,8 +10,16 @@ import { projects } from "./project";
|
||||
import { security } from "./security";
|
||||
import { applicationStatus } from "./shared";
|
||||
import { ports } from "./port";
|
||||
import { boolean, integer, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import {
|
||||
boolean,
|
||||
integer,
|
||||
json,
|
||||
pgEnum,
|
||||
pgTable,
|
||||
text,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { generateAppName } from "./utils";
|
||||
import { registry } from "./registry";
|
||||
|
||||
export const sourceType = pgEnum("sourceType", ["docker", "git", "github"]);
|
||||
|
||||
@@ -22,6 +30,65 @@ export const buildType = pgEnum("buildType", [
|
||||
"nixpacks",
|
||||
]);
|
||||
|
||||
// TODO: refactor this types
|
||||
interface HealthCheckSwarm {
|
||||
Test?: string[] | undefined;
|
||||
Interval?: number | undefined;
|
||||
Timeout?: number | undefined;
|
||||
StartPeriod?: number | undefined;
|
||||
Retries?: number | undefined;
|
||||
}
|
||||
|
||||
interface RestartPolicySwarm {
|
||||
Condition?: string | undefined;
|
||||
Delay?: number | undefined;
|
||||
MaxAttempts?: number | undefined;
|
||||
Window?: number | undefined;
|
||||
}
|
||||
|
||||
interface PlacementSwarm {
|
||||
Constraints?: string[] | undefined;
|
||||
Preferences?: Array<{ Spread: { SpreadDescriptor: string } }> | undefined;
|
||||
MaxReplicas?: number | undefined;
|
||||
Platforms?:
|
||||
| Array<{
|
||||
Architecture: string;
|
||||
OS: string;
|
||||
}>
|
||||
| undefined;
|
||||
}
|
||||
|
||||
interface UpdateConfigSwarm {
|
||||
Parallelism: number;
|
||||
Delay?: number | undefined;
|
||||
FailureAction?: string | undefined;
|
||||
Monitor?: number | undefined;
|
||||
MaxFailureRatio?: number | undefined;
|
||||
Order: string;
|
||||
}
|
||||
|
||||
interface ServiceModeSwarm {
|
||||
Replicated?: { Replicas?: number | undefined } | undefined;
|
||||
Global?: {} | undefined;
|
||||
ReplicatedJob?:
|
||||
| {
|
||||
MaxConcurrent?: number | undefined;
|
||||
TotalCompletions?: number | undefined;
|
||||
}
|
||||
| undefined;
|
||||
GlobalJob?: {} | undefined;
|
||||
}
|
||||
|
||||
interface NetworkSwarm {
|
||||
Target?: string | undefined;
|
||||
Aliases?: string[] | undefined;
|
||||
DriverOpts?: { [key: string]: string } | undefined;
|
||||
}
|
||||
|
||||
interface LabelsSwarm {
|
||||
[name: string]: string;
|
||||
}
|
||||
|
||||
export const applications = pgTable("application", {
|
||||
applicationId: text("applicationId")
|
||||
.notNull()
|
||||
@@ -60,6 +127,17 @@ export const applications = pgTable("application", {
|
||||
customGitBuildPath: text("customGitBuildPath"),
|
||||
customGitSSHKey: text("customGitSSHKey"),
|
||||
dockerfile: text("dockerfile"),
|
||||
// Docker swarm json
|
||||
healthCheckSwarm: json("healthCheckSwarm").$type<HealthCheckSwarm>(),
|
||||
restartPolicySwarm: json("restartPolicySwarm").$type<RestartPolicySwarm>(),
|
||||
placementSwarm: json("placementSwarm").$type<PlacementSwarm>(),
|
||||
updateConfigSwarm: json("updateConfigSwarm").$type<UpdateConfigSwarm>(),
|
||||
rollbackConfigSwarm: json("rollbackConfigSwarm").$type<UpdateConfigSwarm>(),
|
||||
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
|
||||
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
|
||||
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
|
||||
//
|
||||
replicas: integer("replicas").default(1).notNull(),
|
||||
applicationStatus: applicationStatus("applicationStatus")
|
||||
.notNull()
|
||||
.default("idle"),
|
||||
@@ -67,6 +145,9 @@ export const applications = pgTable("application", {
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
registryId: text("registryId").references(() => registry.registryId, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
projectId: text("projectId")
|
||||
.notNull()
|
||||
.references(() => projects.projectId, { onDelete: "cascade" }),
|
||||
@@ -85,9 +166,101 @@ export const applicationsRelations = relations(
|
||||
redirects: many(redirects),
|
||||
security: many(security),
|
||||
ports: many(ports),
|
||||
registry: one(registry, {
|
||||
fields: [applications.registryId],
|
||||
references: [registry.registryId],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
const HealthCheckSwarmSchema = z
|
||||
.object({
|
||||
Test: z.array(z.string()).optional(),
|
||||
Interval: z.number().optional(),
|
||||
Timeout: z.number().optional(),
|
||||
StartPeriod: z.number().optional(),
|
||||
Retries: z.number().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const RestartPolicySwarmSchema = z
|
||||
.object({
|
||||
Condition: z.string().optional(),
|
||||
Delay: z.number().optional(),
|
||||
MaxAttempts: z.number().optional(),
|
||||
Window: z.number().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const PreferenceSchema = z
|
||||
.object({
|
||||
Spread: z.object({
|
||||
SpreadDescriptor: z.string(),
|
||||
}),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const PlatformSchema = z
|
||||
.object({
|
||||
Architecture: z.string(),
|
||||
OS: z.string(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const PlacementSwarmSchema = z
|
||||
.object({
|
||||
Constraints: z.array(z.string()).optional(),
|
||||
Preferences: z.array(PreferenceSchema).optional(),
|
||||
MaxReplicas: z.number().optional(),
|
||||
Platforms: z.array(PlatformSchema).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const UpdateConfigSwarmSchema = z
|
||||
.object({
|
||||
Parallelism: z.number(),
|
||||
Delay: z.number().optional(),
|
||||
FailureAction: z.string().optional(),
|
||||
Monitor: z.number().optional(),
|
||||
MaxFailureRatio: z.number().optional(),
|
||||
Order: z.string(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const ReplicatedSchema = z
|
||||
.object({
|
||||
Replicas: z.number().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const ReplicatedJobSchema = z
|
||||
.object({
|
||||
MaxConcurrent: z.number().optional(),
|
||||
TotalCompletions: z.number().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const ServiceModeSwarmSchema = z
|
||||
.object({
|
||||
Replicated: ReplicatedSchema.optional(),
|
||||
Global: z.object({}).optional(),
|
||||
ReplicatedJob: ReplicatedJobSchema.optional(),
|
||||
GlobalJob: z.object({}).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const NetworkSwarmSchema = z.array(
|
||||
z
|
||||
.object({
|
||||
Target: z.string().optional(),
|
||||
Aliases: z.array(z.string()).optional(),
|
||||
DriverOpts: z.object({}).optional(),
|
||||
})
|
||||
.strict(),
|
||||
);
|
||||
|
||||
const LabelsSwarmSchema = z.record(z.string());
|
||||
|
||||
const createSchema = createInsertSchema(applications, {
|
||||
appName: z.string(),
|
||||
createdAt: z.string(),
|
||||
@@ -124,6 +297,14 @@ const createSchema = createInsertSchema(applications, {
|
||||
"nixpacks",
|
||||
]),
|
||||
owner: z.string(),
|
||||
healthCheckSwarm: HealthCheckSwarmSchema.nullable(),
|
||||
restartPolicySwarm: RestartPolicySwarmSchema.nullable(),
|
||||
placementSwarm: PlacementSwarmSchema.nullable(),
|
||||
updateConfigSwarm: UpdateConfigSwarmSchema.nullable(),
|
||||
rollbackConfigSwarm: UpdateConfigSwarmSchema.nullable(),
|
||||
modeSwarm: ServiceModeSwarmSchema.nullable(),
|
||||
labelsSwarm: LabelsSwarmSchema.nullable(),
|
||||
networkSwarm: NetworkSwarmSchema.nullable(),
|
||||
});
|
||||
|
||||
export const apiCreateApplication = createSchema.pick({
|
||||
|
||||
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();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
export * from "./application";
|
||||
export * from "./postgres";
|
||||
|
||||
export * from "./user";
|
||||
export * from "./admin";
|
||||
export * from "./auth";
|
||||
@@ -20,3 +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],
|
||||
|
||||
98
server/db/schema/registry.ts
Normal file
98
server/db/schema/registry.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { relations, sql } from "drizzle-orm";
|
||||
import { boolean, pgEnum, pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
||||
import { auth } from "./auth";
|
||||
import { admins } from "./admin";
|
||||
import { z } from "zod";
|
||||
import { applications } from "./application";
|
||||
/**
|
||||
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
|
||||
* database instance for multiple projects.
|
||||
*
|
||||
* @see https://orm.drizzle.team/docs/goodies#multi-project-schema
|
||||
*/
|
||||
export const registryType = pgEnum("RegistryType", ["selfHosted", "cloud"]);
|
||||
|
||||
export const registry = pgTable("registry", {
|
||||
registryId: text("registryId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
registryName: text("registryName").notNull(),
|
||||
imagePrefix: text("imagePrefix"),
|
||||
username: text("username").notNull(),
|
||||
password: text("password").notNull(),
|
||||
registryUrl: text("registryUrl").notNull(),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
registryType: registryType("selfHosted").notNull().default("cloud"),
|
||||
adminId: text("adminId")
|
||||
.notNull()
|
||||
.references(() => admins.adminId, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
export const registryRelations = relations(registry, ({ one, many }) => ({
|
||||
admin: one(admins, {
|
||||
fields: [registry.adminId],
|
||||
references: [admins.adminId],
|
||||
}),
|
||||
applications: many(applications),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(registry, {
|
||||
registryName: z.string().min(1),
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
registryUrl: z.string().min(1),
|
||||
adminId: z.string().min(1),
|
||||
registryId: z.string().min(1),
|
||||
registryType: z.enum(["selfHosted", "cloud"]),
|
||||
imagePrefix: z.string().nullable().optional(),
|
||||
});
|
||||
|
||||
export const apiCreateRegistry = createSchema
|
||||
.pick({})
|
||||
.extend({
|
||||
registryName: z.string().min(1),
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
registryUrl: z.string(),
|
||||
registryType: z.enum(["selfHosted", "cloud"]),
|
||||
imagePrefix: z.string().nullable().optional(),
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiTestRegistry = createSchema.pick({}).extend({
|
||||
registryName: z.string().min(1),
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
registryUrl: z.string(),
|
||||
registryType: z.enum(["selfHosted", "cloud"]),
|
||||
imagePrefix: z.string().nullable().optional(),
|
||||
});
|
||||
|
||||
export const apiRemoveRegistry = createSchema
|
||||
.pick({
|
||||
registryId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiFindOneRegistry = createSchema
|
||||
.pick({
|
||||
registryId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiUpdateRegistry = createSchema.partial().extend({
|
||||
registryId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiEnableSelfHostedRegistry = createSchema
|
||||
.pick({
|
||||
registryUrl: true,
|
||||
username: true,
|
||||
password: true,
|
||||
})
|
||||
.required();
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import { setupDeploymentLogsWebSocketServer } from "./wss/listen-deployment";
|
||||
import { setupDockerStatsMonitoringSocketServer } from "./wss/docker-stats";
|
||||
import { setupDirectories } from "./setup/config-paths";
|
||||
import { initializeNetwork, initializeSwarm } from "./setup/setup";
|
||||
import { initializeNetwork } from "./setup/setup";
|
||||
import {
|
||||
createDefaultMiddlewares,
|
||||
createDefaultServerTraefikConfig,
|
||||
@@ -44,7 +44,6 @@ void app.prepare().then(async () => {
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
setupDirectories();
|
||||
createDefaultMiddlewares();
|
||||
await initializeSwarm();
|
||||
await initializeNetwork();
|
||||
createDefaultTraefikConfig();
|
||||
createDefaultServerTraefikConfig();
|
||||
|
||||
@@ -33,7 +33,9 @@ export const initializeRedis = async () => {
|
||||
Ports: [
|
||||
{
|
||||
TargetPort: 6379,
|
||||
PublishedPort: 6379,
|
||||
...(process.env.NODE_ENV === "development"
|
||||
? { PublishedPort: 6379 }
|
||||
: {}),
|
||||
Protocol: "tcp",
|
||||
},
|
||||
],
|
||||
|
||||
89
server/setup/registry-setup.ts
Normal file
89
server/setup/registry-setup.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import type { CreateServiceOptions } from "dockerode";
|
||||
import { docker, REGISTRY_PATH } from "../constants";
|
||||
import { pullImage } from "../utils/docker/utils";
|
||||
import { execAsync } from "../utils/process/execAsync";
|
||||
import { generateRandomPassword } from "../auth/random-password";
|
||||
|
||||
export const initializeRegistry = async (
|
||||
username: string,
|
||||
password: string,
|
||||
) => {
|
||||
const imageName = "registry:2.8.3";
|
||||
const containerName = "dokploy-registry";
|
||||
await generateRegistryPassword(username, password);
|
||||
const randomPass = await generateRandomPassword();
|
||||
const settings: CreateServiceOptions = {
|
||||
Name: containerName,
|
||||
TaskTemplate: {
|
||||
ContainerSpec: {
|
||||
Image: imageName,
|
||||
Env: [
|
||||
"REGISTRY_STORAGE_DELETE_ENABLED=true",
|
||||
"REGISTRY_AUTH=htpasswd",
|
||||
"REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm",
|
||||
"REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd",
|
||||
`REGISTRY_HTTP_SECRET=${randomPass.hashedPassword}`,
|
||||
],
|
||||
Mounts: [
|
||||
{
|
||||
Type: "bind",
|
||||
Source: `${REGISTRY_PATH}/htpasswd`,
|
||||
Target: "/auth/htpasswd",
|
||||
ReadOnly: true,
|
||||
},
|
||||
{
|
||||
Type: "volume",
|
||||
Source: "registry-data",
|
||||
Target: "/var/lib/registry",
|
||||
ReadOnly: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
Networks: [{ Target: "dokploy-network" }],
|
||||
RestartPolicy: {
|
||||
Condition: "on-failure",
|
||||
},
|
||||
},
|
||||
Mode: {
|
||||
Replicated: {
|
||||
Replicas: 1,
|
||||
},
|
||||
},
|
||||
EndpointSpec: {
|
||||
Ports: [
|
||||
{
|
||||
TargetPort: 5000,
|
||||
PublishedPort: 5000,
|
||||
Protocol: "tcp",
|
||||
PublishMode: "host",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
try {
|
||||
await pullImage(imageName);
|
||||
|
||||
const service = docker.getService(containerName);
|
||||
const inspect = await service.inspect();
|
||||
await service.update({
|
||||
version: Number.parseInt(inspect.Version.Index),
|
||||
...settings,
|
||||
});
|
||||
console.log("Registry Started ✅");
|
||||
} catch (error) {
|
||||
await docker.createService(settings);
|
||||
console.log("Registry Not Found: Starting ✅");
|
||||
}
|
||||
};
|
||||
|
||||
const generateRegistryPassword = async (username: string, password: string) => {
|
||||
try {
|
||||
const command = `htpasswd -nbB ${username} "${password}" > ${REGISTRY_PATH}/htpasswd`;
|
||||
const result = await execAsync(command);
|
||||
console.log("Password generated ✅");
|
||||
return result.stdout.trim();
|
||||
} catch (error) {
|
||||
console.error("Error generating password:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -7,6 +7,10 @@ import type { MainTraefikConfig } from "../utils/traefik/types";
|
||||
import type { FileConfig } from "../utils/traefik/file-types";
|
||||
import type { CreateServiceOptions } from "dockerode";
|
||||
|
||||
const TRAEFIK_SSL_PORT =
|
||||
Number.parseInt(process.env.TRAEFIK_SSL_PORT ?? "", 10) || 443;
|
||||
const TRAEFIK_PORT = Number.parseInt(process.env.TRAEFIK_PORT ?? "", 10) || 80;
|
||||
|
||||
export const initializeTraefik = async () => {
|
||||
const imageName = "traefik:v2.5";
|
||||
const containerName = "dokploy-traefik";
|
||||
@@ -47,12 +51,12 @@ export const initializeTraefik = async () => {
|
||||
Ports: [
|
||||
{
|
||||
TargetPort: 443,
|
||||
PublishedPort: 443,
|
||||
PublishedPort: TRAEFIK_SSL_PORT,
|
||||
PublishMode: "host",
|
||||
},
|
||||
{
|
||||
TargetPort: 80,
|
||||
PublishedPort: 80,
|
||||
PublishedPort: TRAEFIK_PORT,
|
||||
PublishMode: "host",
|
||||
},
|
||||
{
|
||||
@@ -127,11 +131,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,
|
||||
@@ -139,21 +150,10 @@ export const createDefaultTraefikConfig = () => {
|
||||
},
|
||||
entryPoints: {
|
||||
web: {
|
||||
address: ":80",
|
||||
...(process.env.NODE_ENV === "production" && {
|
||||
http: {
|
||||
redirections: {
|
||||
entryPoint: {
|
||||
to: "websecure",
|
||||
scheme: "https",
|
||||
permanent: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
address: `:${TRAEFIK_PORT}`,
|
||||
},
|
||||
websecure: {
|
||||
address: ":443",
|
||||
address: `:${TRAEFIK_SSL_PORT}`,
|
||||
...(process.env.NODE_ENV === "production" && {
|
||||
http: {
|
||||
tls: {
|
||||
|
||||
122
server/utils/builders/compose.ts
Normal file
122
server/utils/builders/compose.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
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");
|
||||
let envContent = env || "";
|
||||
if (!envContent.includes("DOCKER_CONFIG")) {
|
||||
envContent += "\nDOCKER_CONFIG=/root/.docker/config.json";
|
||||
}
|
||||
|
||||
const envFileContent = prepareEnvironmentVariables(envContent).join("\n");
|
||||
|
||||
if (!existsSync(dirname(envFilePath))) {
|
||||
mkdirSync(dirname(envFilePath), { recursive: true });
|
||||
}
|
||||
writeFileSync(envFilePath, envFileContent);
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import type { CreateServiceOptions } from "dockerode";
|
||||
import {
|
||||
calculateResources,
|
||||
generateBindMounts,
|
||||
generateConfigContainer,
|
||||
generateFileMounts,
|
||||
generateVolumeMounts,
|
||||
prepareEnvironmentVariables,
|
||||
@@ -13,6 +14,7 @@ import { buildCustomDocker } from "./docker-file";
|
||||
import { buildHeroku } from "./heroku";
|
||||
import { buildNixpacks } from "./nixpacks";
|
||||
import { buildPaketo } from "./paketo";
|
||||
import { uploadImage } from "../cluster/upload";
|
||||
|
||||
// NIXPACKS codeDirectory = where is the path of the code directory
|
||||
// HEROKU codeDirectory = where is the path of the code directory
|
||||
@@ -20,7 +22,7 @@ import { buildPaketo } from "./paketo";
|
||||
// DOCKERFILE codeDirectory = where is the exact path of the (Dockerfile)
|
||||
export type ApplicationNested = InferResultType<
|
||||
"applications",
|
||||
{ mounts: true; security: true; redirects: true; ports: true }
|
||||
{ mounts: true; security: true; redirects: true; ports: true; registry: true }
|
||||
>;
|
||||
export const buildApplication = async (
|
||||
application: ApplicationNested,
|
||||
@@ -42,6 +44,10 @@ export const buildApplication = async (
|
||||
} else if (buildType === "dockerfile") {
|
||||
await buildCustomDocker(application, writeStream);
|
||||
}
|
||||
|
||||
if (application.registryId) {
|
||||
await uploadImage(application, writeStream);
|
||||
}
|
||||
await mechanizeDockerContainer(application);
|
||||
writeStream.write("Docker Deployed: ✅");
|
||||
} catch (error) {
|
||||
@@ -59,8 +65,6 @@ export const mechanizeDockerContainer = async (
|
||||
appName,
|
||||
env,
|
||||
mounts,
|
||||
sourceType,
|
||||
dockerImage,
|
||||
cpuLimit,
|
||||
memoryLimit,
|
||||
memoryReservation,
|
||||
@@ -75,16 +79,34 @@ export const mechanizeDockerContainer = async (
|
||||
cpuLimit,
|
||||
cpuReservation,
|
||||
});
|
||||
|
||||
const volumesMount = generateVolumeMounts(mounts);
|
||||
|
||||
const {
|
||||
HealthCheck,
|
||||
RestartPolicy,
|
||||
Placement,
|
||||
Labels,
|
||||
Mode,
|
||||
RollbackConfig,
|
||||
UpdateConfig,
|
||||
Networks,
|
||||
} = generateConfigContainer(application);
|
||||
|
||||
const bindsMount = generateBindMounts(mounts);
|
||||
const filesMount = generateFileMounts(appName, mounts);
|
||||
const envVariables = prepareEnvironmentVariables(env);
|
||||
|
||||
const image = getImageName(application);
|
||||
const authConfig = getAuthConfig(application);
|
||||
|
||||
const settings: CreateServiceOptions = {
|
||||
authconfig: authConfig,
|
||||
Name: appName,
|
||||
TaskTemplate: {
|
||||
ContainerSpec: {
|
||||
Image: sourceType === "docker" ? dockerImage! : `${appName}:latest`,
|
||||
HealthCheck,
|
||||
Image: image,
|
||||
Env: envVariables,
|
||||
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
|
||||
...(command
|
||||
@@ -93,20 +115,17 @@ export const mechanizeDockerContainer = async (
|
||||
Args: ["-c", command],
|
||||
}
|
||||
: {}),
|
||||
Labels,
|
||||
},
|
||||
Networks: [{ Target: "dokploy-network" }],
|
||||
RestartPolicy: {
|
||||
Condition: "on-failure",
|
||||
},
|
||||
Networks,
|
||||
RestartPolicy,
|
||||
Placement,
|
||||
Resources: {
|
||||
...resources,
|
||||
},
|
||||
},
|
||||
Mode: {
|
||||
Replicated: {
|
||||
Replicas: 1,
|
||||
},
|
||||
},
|
||||
Mode,
|
||||
RollbackConfig,
|
||||
EndpointSpec: {
|
||||
Ports: ports.map((port) => ({
|
||||
Protocol: port.protocol,
|
||||
@@ -114,10 +133,7 @@ export const mechanizeDockerContainer = async (
|
||||
PublishedPort: port.publishedPort,
|
||||
})),
|
||||
},
|
||||
UpdateConfig: {
|
||||
Parallelism: 1,
|
||||
Order: "start-first",
|
||||
},
|
||||
UpdateConfig,
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -132,7 +148,43 @@ export const mechanizeDockerContainer = async (
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
await docker.createService(settings);
|
||||
}
|
||||
// await cleanUpUnusedImages();
|
||||
};
|
||||
|
||||
const getImageName = (application: ApplicationNested) => {
|
||||
const { appName, sourceType, dockerImage, registry } = application;
|
||||
|
||||
if (sourceType === "docker") {
|
||||
return dockerImage || "ERROR-NO-IMAGE-PROVIDED";
|
||||
}
|
||||
|
||||
const registryUrl = registry?.registryUrl || "";
|
||||
const imagePrefix = registry?.imagePrefix ? `${registry.imagePrefix}/` : "";
|
||||
return registry
|
||||
? `${registryUrl}/${imagePrefix}${appName}`
|
||||
: `${appName}:latest`;
|
||||
};
|
||||
|
||||
const getAuthConfig = (application: ApplicationNested) => {
|
||||
const { registry, username, password, sourceType } = application;
|
||||
|
||||
if (sourceType === "docker") {
|
||||
if (username && password) {
|
||||
return {
|
||||
password,
|
||||
username,
|
||||
serveraddress: "https://index.docker.io/v1/",
|
||||
};
|
||||
}
|
||||
} else if (registry) {
|
||||
return {
|
||||
password: registry.password,
|
||||
username: registry.username,
|
||||
serveraddress: registry.registryUrl,
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
65
server/utils/cluster/upload.ts
Normal file
65
server/utils/cluster/upload.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import type { ApplicationNested } from "../builders";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
import type { WriteStream } from "node:fs";
|
||||
|
||||
export const uploadImage = async (
|
||||
application: ApplicationNested,
|
||||
writeStream: WriteStream,
|
||||
) => {
|
||||
const registry = application.registry;
|
||||
|
||||
if (!registry) {
|
||||
throw new Error("Registry not found");
|
||||
}
|
||||
|
||||
const { registryUrl, imagePrefix, registryType } = registry;
|
||||
const { appName } = application;
|
||||
const imageName = `${appName}:latest`;
|
||||
|
||||
const finalURL =
|
||||
registryType === "selfHosted"
|
||||
? process.env.NODE_ENV === "development"
|
||||
? "localhost:5000"
|
||||
: registryUrl
|
||||
: registryUrl;
|
||||
|
||||
const registryTag = imagePrefix
|
||||
? `${finalURL}/${imagePrefix}/${imageName}`
|
||||
: `${finalURL}/${imageName}`;
|
||||
|
||||
try {
|
||||
console.log(finalURL, registryTag);
|
||||
writeStream.write(
|
||||
`📦 [Enabled Registry] Uploading image to ${registry.registryType} | ${registryTag} | ${finalURL}\n`,
|
||||
);
|
||||
|
||||
await spawnAsync(
|
||||
"docker",
|
||||
["login", finalURL, "-u", registry.username, "-p", registry.password],
|
||||
(data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
await spawnAsync("docker", ["tag", imageName, registryTag], (data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
});
|
||||
|
||||
await spawnAsync("docker", ["push", registryTag], (data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
// docker:
|
||||
// endpoint: "unix:///var/run/docker.sock"
|
||||
// exposedByDefault: false
|
||||
// swarmMode: true
|
||||
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";
|
||||
@@ -122,10 +122,10 @@ export const cleanUpInactiveContainers = async () => {
|
||||
|
||||
for (const container of inactiveContainers) {
|
||||
await docker.getContainer(container.Id).remove({ force: true });
|
||||
console.log(`Contenedor eliminado: ${container.Id}`);
|
||||
console.log(`Cleaning up inactive container: ${container.Id}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error al limpiar contenedores inactivos:", error);
|
||||
console.error("Error cleaning up inactive containers:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -199,6 +199,83 @@ export const calculateResources = ({
|
||||
};
|
||||
};
|
||||
|
||||
export const generateConfigContainer = (application: ApplicationNested) => {
|
||||
const {
|
||||
healthCheckSwarm,
|
||||
restartPolicySwarm,
|
||||
placementSwarm,
|
||||
updateConfigSwarm,
|
||||
rollbackConfigSwarm,
|
||||
modeSwarm,
|
||||
labelsSwarm,
|
||||
replicas,
|
||||
mounts,
|
||||
networkSwarm,
|
||||
} = application;
|
||||
|
||||
const haveMounts = mounts.length > 0;
|
||||
|
||||
return {
|
||||
...(healthCheckSwarm && {
|
||||
HealthCheck: healthCheckSwarm,
|
||||
}),
|
||||
...(restartPolicySwarm
|
||||
? {
|
||||
RestartPolicy: restartPolicySwarm,
|
||||
}
|
||||
: {
|
||||
// if no restartPolicySwarm provided use default
|
||||
RestartPolicy: {
|
||||
Condition: "on-failure",
|
||||
},
|
||||
}),
|
||||
...(placementSwarm
|
||||
? {
|
||||
Placement: placementSwarm,
|
||||
}
|
||||
: {
|
||||
// if app have mounts keep manager as constraint
|
||||
Placement: {
|
||||
Constraints: haveMounts ? ["node.role==manager"] : [],
|
||||
},
|
||||
}),
|
||||
...(labelsSwarm && {
|
||||
Labels: labelsSwarm,
|
||||
}),
|
||||
...(modeSwarm
|
||||
? {
|
||||
Mode: modeSwarm,
|
||||
}
|
||||
: {
|
||||
// use replicas value if no modeSwarm provided
|
||||
Mode: {
|
||||
Replicated: {
|
||||
Replicas: replicas,
|
||||
},
|
||||
},
|
||||
}),
|
||||
...(rollbackConfigSwarm && {
|
||||
RollbackConfig: rollbackConfigSwarm,
|
||||
}),
|
||||
...(updateConfigSwarm
|
||||
? { UpdateConfig: updateConfigSwarm }
|
||||
: {
|
||||
// default config if no updateConfigSwarm provided
|
||||
UpdateConfig: {
|
||||
Parallelism: 1,
|
||||
Order: "start-first",
|
||||
},
|
||||
}),
|
||||
...(networkSwarm
|
||||
? {
|
||||
Networks: networkSwarm,
|
||||
}
|
||||
: {
|
||||
Networks: [{ Target: "dokploy-network" }],
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
export const generateBindMounts = (mounts: ApplicationNested["mounts"]) => {
|
||||
if (!mounts || mounts.length === 0) {
|
||||
return [];
|
||||
@@ -247,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();
|
||||
}
|
||||
};
|
||||
@@ -47,10 +47,7 @@ export const removeDomain = async (appName: string, uniqueKey: number) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const createRouterConfig = async (
|
||||
app: ApplicationNested,
|
||||
domain: Domain,
|
||||
) => {
|
||||
const createRouterConfig = async (app: ApplicationNested, domain: Domain) => {
|
||||
const { appName, redirects, security } = app;
|
||||
const { certificateType } = domain;
|
||||
|
||||
|
||||
72
server/utils/traefik/registry.ts
Normal file
72
server/utils/traefik/registry.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import type { FileConfig, HttpRouter } from "./file-types";
|
||||
import type { Registry } from "@/server/api/services/registry";
|
||||
import { removeDirectoryIfExistsContent } from "../filesystem/directory";
|
||||
import { REGISTRY_PATH } from "@/server/constants";
|
||||
import { dump, load } from "js-yaml";
|
||||
import { join } from "node:path";
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
|
||||
export const manageRegistry = async (registry: Registry) => {
|
||||
if (!existsSync(REGISTRY_PATH)) {
|
||||
mkdirSync(REGISTRY_PATH, { recursive: true });
|
||||
}
|
||||
|
||||
const appName = "dokploy-registry";
|
||||
const config: FileConfig = loadOrCreateConfig();
|
||||
|
||||
const serviceName = `${appName}-service`;
|
||||
const routerName = `${appName}-router`;
|
||||
|
||||
config.http = config.http || { routers: {}, services: {} };
|
||||
config.http.routers = config.http.routers || {};
|
||||
config.http.services = config.http.services || {};
|
||||
|
||||
config.http.routers[routerName] = await createRegistryRouterConfig(registry);
|
||||
|
||||
config.http.services[serviceName] = {
|
||||
loadBalancer: {
|
||||
servers: [{ url: `http://${appName}:5000` }],
|
||||
passHostHeader: true,
|
||||
},
|
||||
};
|
||||
|
||||
const yamlConfig = dump(config);
|
||||
const configFile = join(REGISTRY_PATH, "registry.yml");
|
||||
writeFileSync(configFile, yamlConfig);
|
||||
};
|
||||
|
||||
export const removeSelfHostedRegistry = async () => {
|
||||
await removeDirectoryIfExistsContent(REGISTRY_PATH);
|
||||
};
|
||||
|
||||
const createRegistryRouterConfig = async (registry: Registry) => {
|
||||
const { registryUrl } = registry;
|
||||
const routerConfig: HttpRouter = {
|
||||
rule: `Host(\`${registryUrl}\`)`,
|
||||
service: "dokploy-registry-service",
|
||||
middlewares: ["redirect-to-https"],
|
||||
entryPoints: [
|
||||
"web",
|
||||
...(process.env.NODE_ENV === "production" ? ["websecure"] : []),
|
||||
],
|
||||
...(process.env.NODE_ENV === "production"
|
||||
? {
|
||||
tls: { certResolver: "letsencrypt" },
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
return routerConfig;
|
||||
};
|
||||
|
||||
const loadOrCreateConfig = (): FileConfig => {
|
||||
const configPath = join(REGISTRY_PATH, "registry.yml");
|
||||
if (existsSync(configPath)) {
|
||||
const yamlStr = readFileSync(configPath, "utf8");
|
||||
const parsedConfig = (load(yamlStr) as FileConfig) || {
|
||||
http: { routers: {}, services: {} },
|
||||
};
|
||||
return parsedConfig;
|
||||
}
|
||||
return { http: { routers: {}, services: {} } };
|
||||
};
|
||||
@@ -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