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:
@@ -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) {}
|
||||
|
||||
Reference in New Issue
Block a user