fix: resolved merge conflicts with fork/canary

This commit is contained in:
Jason Parks
2025-03-16 03:02:15 -06:00
502 changed files with 18288 additions and 16475 deletions

View File

@@ -345,6 +345,7 @@ export const applicationRouter = createTRPCRouter({
buildPath: input.buildPath,
applicationStatus: "idle",
githubId: input.githubId,
watchPaths: input.watchPaths,
});
return true;
@@ -371,6 +372,7 @@ export const applicationRouter = createTRPCRouter({
gitlabId: input.gitlabId,
gitlabProjectId: input.gitlabProjectId,
gitlabPathNamespace: input.gitlabPathNamespace,
watchPaths: input.watchPaths,
});
return true;
@@ -395,6 +397,7 @@ export const applicationRouter = createTRPCRouter({
sourceType: "bitbucket",
applicationStatus: "idle",
bitbucketId: input.bitbucketId,
watchPaths: input.watchPaths,
});
return true;
@@ -467,6 +470,7 @@ export const applicationRouter = createTRPCRouter({
customGitSSHKeyId: input.customGitSSHKeyId,
sourceType: "git",
applicationStatus: "idle",
watchPaths: input.watchPaths,
});
return true;
@@ -695,4 +699,49 @@ export const applicationRouter = createTRPCRouter({
return stats;
}),
move: protectedProcedure
.input(
z.object({
applicationId: z.string(),
targetProjectId: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
application.project.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this application",
});
}
const targetProject = await findProjectById(input.targetProjectId);
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this project",
});
}
// Update the application's projectId
const updatedApplication = await db
.update(applications)
.set({
projectId: input.targetProjectId,
})
.where(eq(applications.applicationId, input.applicationId))
.returning()
.then((res) => res[0]);
if (!updatedApplication) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to move application",
});
}
return updatedApplication;
}),
});

View File

@@ -1,22 +1,35 @@
import { getPublicIpWithFallback } from "@/server/wss/terminal";
import { type DockerNode, IS_CLOUD, docker, execAsync } from "@dokploy/server";
import {
type DockerNode,
IS_CLOUD,
execAsync,
getRemoteDocker,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc";
export const clusterRouter = createTRPCRouter({
getNodes: protectedProcedure.query(async () => {
if (IS_CLOUD) {
return [];
}
const workers: DockerNode[] = await docker.listNodes();
getNodes: protectedProcedure
.input(
z.object({
serverId: z.string().optional(),
}),
)
.query(async ({ input }) => {
if (IS_CLOUD) {
return [];
}
return workers;
}),
const docker = await getRemoteDocker(input.serverId);
const workers: DockerNode[] = await docker.listNodes();
return workers;
}),
removeWorker: protectedProcedure
.input(
z.object({
nodeId: z.string(),
serverId: z.string().optional(),
}),
)
.mutation(async ({ input }) => {
@@ -40,37 +53,51 @@ export const clusterRouter = createTRPCRouter({
});
}
}),
addWorker: protectedProcedure.query(async () => {
if (IS_CLOUD) {
return {
command: "",
version: "",
};
}
const result = await docker.swarmInspect();
const docker_version = await docker.version();
addWorker: protectedProcedure
.input(
z.object({
serverId: z.string().optional(),
}),
)
.query(async ({ input }) => {
if (IS_CLOUD) {
return {
command: "",
version: "",
};
}
const docker = await getRemoteDocker(input.serverId);
const result = await docker.swarmInspect();
const docker_version = await docker.version();
return {
command: `docker swarm join --token ${
result.JoinTokens.Worker
} ${await getPublicIpWithFallback()}:2377`,
version: docker_version.Version,
};
}),
addManager: protectedProcedure.query(async () => {
if (IS_CLOUD) {
return {
command: "",
version: "",
command: `docker swarm join --token ${
result.JoinTokens.Worker
} ${await getPublicIpWithFallback()}:2377`,
version: docker_version.Version,
};
}
const result = await docker.swarmInspect();
const docker_version = await docker.version();
return {
command: `docker swarm join --token ${
result.JoinTokens.Manager
} ${await getPublicIpWithFallback()}:2377`,
version: docker_version.Version,
};
}),
}),
addManager: protectedProcedure
.input(
z.object({
serverId: z.string().optional(),
}),
)
.query(async ({ input }) => {
if (IS_CLOUD) {
return {
command: "",
version: "",
};
}
const docker = await getRemoteDocker(input.serverId);
const result = await docker.swarmInspect();
const docker_version = await docker.version();
return {
command: `docker swarm join --token ${
result.JoinTokens.Manager
} ${await getPublicIpWithFallback()}:2377`,
version: docker_version.Version,
};
}),
});

View File

@@ -2,29 +2,28 @@ import { slugify } from "@/lib/slug";
import { db } from "@/server/db";
import {
apiCreateCompose,
apiCreateComposeByTemplate,
apiDeleteCompose,
apiFetchServices,
apiFindCompose,
apiRandomizeCompose,
apiUpdateCompose,
compose,
compose as composeTable,
} from "@/server/db/schema";
import { cleanQueuesByCompose, myQueue } from "@/server/queues/queueSetup";
import { templates } from "@/templates/templates";
import type { TemplatesKeys } from "@/templates/types/templates-data.type";
import { generatePassword } from "@/templates/utils";
import {
generatePassword,
loadTemplateModule,
readTemplateComposeFile,
} from "@/templates/utils";
type CompleteTemplate,
fetchTemplateFiles,
fetchTemplatesList,
} from "@dokploy/server/templates/github";
import { processTemplate } from "@dokploy/server/templates/processors";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { dump } from "js-yaml";
import { dump, load } from "js-yaml";
import _ from "lodash";
import { nanoid } from "nanoid";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
import { z } from "zod";
import type { DeploymentJob } from "@/server/queues/queue-types";
import { deploy } from "@/server/utils/deploy";
import {
@@ -39,6 +38,7 @@ import {
createComposeByTemplate,
createDomain,
createMount,
deleteMount,
findComposeById,
findDomainsByComposeId,
findProjectById,
@@ -50,6 +50,7 @@ import {
removeCompose,
removeComposeDirectory,
removeDeploymentsByComposeId,
removeDomainById,
startCompose,
stopCompose,
updateCompose,
@@ -157,8 +158,8 @@ export const composeRouter = createTRPCRouter({
4;
const result = await db
.delete(compose)
.where(eq(compose.composeId, input.composeId))
.delete(composeTable)
.where(eq(composeTable.composeId, input.composeId))
.returning();
const cleanupOperations = [
@@ -397,7 +398,14 @@ export const composeRouter = createTRPCRouter({
return true;
}),
deployTemplate: protectedProcedure
.input(apiCreateComposeByTemplate)
.input(
z.object({
projectId: z.string(),
serverId: z.string().optional(),
id: z.string(),
baseUrl: z.string().optional(),
}),
)
.mutation(async ({ ctx, input }) => {
if (ctx.user.rol === "member") {
await checkServiceAccess(
@@ -415,9 +423,7 @@ export const composeRouter = createTRPCRouter({
});
}
const composeFile = await readTemplateComposeFile(input.id);
const generate = await loadTemplateModule(input.id as TemplatesKeys);
const template = await fetchTemplateFiles(input.id, input.baseUrl);
const admin = await findUserById(ctx.user.ownerId);
let serverIp = admin.serverIp || "127.0.0.1";
@@ -430,16 +436,17 @@ export const composeRouter = createTRPCRouter({
} else if (process.env.NODE_ENV === "development") {
serverIp = "127.0.0.1";
}
const projectName = slugify(`${project.name} ${input.id}`);
const { envs, mounts, domains } = generate({
serverIp: serverIp || "",
const generate = processTemplate(template.config, {
serverIp: serverIp,
projectName: projectName,
});
const compose = await createComposeByTemplate({
...input,
composeFile: composeFile,
env: envs?.join("\n"),
composeFile: template.dockerCompose,
env: generate.envs?.join("\n"),
serverId: input.serverId,
name: input.id,
sourceType: "raw",
@@ -451,12 +458,12 @@ export const composeRouter = createTRPCRouter({
await addNewService(
ctx.user.id,
compose.composeId,
project.organizationId,
ctx.session.activeOrganizationId,
);
}
if (mounts && mounts?.length > 0) {
for (const mount of mounts) {
if (generate.mounts && generate.mounts?.length > 0) {
for (const mount of generate.mounts) {
await createMount({
filePath: mount.filePath,
mountPath: "",
@@ -468,13 +475,14 @@ export const composeRouter = createTRPCRouter({
}
}
if (domains && domains?.length > 0) {
for (const domain of domains) {
if (generate.domains && generate.domains?.length > 0) {
for (const domain of generate.domains) {
await createDomain({
...domain,
domainType: "compose",
certificateType: "none",
composeId: compose.composeId,
host: domain.host || "",
});
}
}
@@ -482,23 +490,238 @@ export const composeRouter = createTRPCRouter({
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,
}));
templates: publicProcedure
.input(z.object({ baseUrl: z.string().optional() }))
.query(async ({ input }) => {
try {
const githubTemplates = await fetchTemplatesList(input.baseUrl);
return templatesData;
}),
if (githubTemplates.length > 0) {
return githubTemplates;
}
} catch (error) {
console.warn(
"Failed to fetch templates from GitHub, falling back to local templates:",
error,
);
}
return [];
}),
getTags: protectedProcedure.query(async () => {
const allTags = templates.flatMap((template) => template.tags);
const uniqueTags = _.uniq(allTags);
return uniqueTags;
}),
getTags: protectedProcedure
.input(z.object({ baseUrl: z.string().optional() }))
.query(async ({ input }) => {
const githubTemplates = await fetchTemplatesList(input.baseUrl);
const allTags = githubTemplates.flatMap((template) => template.tags);
const uniqueTags = _.uniq(allTags);
return uniqueTags;
}),
move: protectedProcedure
.input(
z.object({
composeId: z.string(),
targetProjectId: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this compose",
});
}
const targetProject = await findProjectById(input.targetProjectId);
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this project",
});
}
const updatedCompose = await db
.update(composeTable)
.set({
projectId: input.targetProjectId,
})
.where(eq(composeTable.composeId, input.composeId))
.returning()
.then((res) => res[0]);
if (!updatedCompose) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to move compose",
});
}
return updatedCompose;
}),
processTemplate: protectedProcedure
.input(
z.object({
base64: z.string(),
composeId: z.string().min(1),
}),
)
.mutation(async ({ input, ctx }) => {
try {
const compose = await findComposeById(input.composeId);
if (
compose.project.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this compose",
});
}
const decodedData = Buffer.from(input.base64, "base64").toString(
"utf-8",
);
const admin = await findUserById(ctx.user.ownerId);
let serverIp = admin.serverIp || "127.0.0.1";
if (compose.serverId) {
const server = await findServerById(compose.serverId);
serverIp = server.ipAddress;
} else if (process.env.NODE_ENV === "development") {
serverIp = "127.0.0.1";
}
const templateData = JSON.parse(decodedData);
const config = load(templateData.config) as CompleteTemplate;
if (!templateData.compose || !config) {
throw new TRPCError({
code: "BAD_REQUEST",
message:
"Invalid template format. Must contain compose and config fields",
});
}
const processedTemplate = processTemplate(config, {
serverIp: serverIp,
projectName: compose.appName,
});
return {
compose: templateData.compose,
template: processedTemplate,
};
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `Error processing template: ${error instanceof Error ? error.message : error}`,
});
}
}),
import: protectedProcedure
.input(
z.object({
base64: z.string(),
composeId: z.string().min(1),
}),
)
.mutation(async ({ input, ctx }) => {
try {
const compose = await findComposeById(input.composeId);
const decodedData = Buffer.from(input.base64, "base64").toString(
"utf-8",
);
if (
compose.project.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this compose",
});
}
for (const mount of compose.mounts) {
await deleteMount(mount.mountId);
}
for (const domain of compose.domains) {
await removeDomainById(domain.domainId);
}
const admin = await findUserById(ctx.user.ownerId);
let serverIp = admin.serverIp || "127.0.0.1";
if (compose.serverId) {
const server = await findServerById(compose.serverId);
serverIp = server.ipAddress;
} else if (process.env.NODE_ENV === "development") {
serverIp = "127.0.0.1";
}
const templateData = JSON.parse(decodedData);
const config = load(templateData.config) as CompleteTemplate;
if (!templateData.compose || !config) {
throw new TRPCError({
code: "BAD_REQUEST",
message:
"Invalid template format. Must contain compose and config fields",
});
}
const processedTemplate = processTemplate(config, {
serverIp: serverIp,
projectName: compose.appName,
});
// Update compose file
await updateCompose(input.composeId, {
composeFile: templateData.compose,
sourceType: "raw",
env: processedTemplate.envs?.join("\n"),
isolatedDeployment: true,
});
// Create mounts
if (processedTemplate.mounts && processedTemplate.mounts.length > 0) {
for (const mount of processedTemplate.mounts) {
await createMount({
filePath: mount.filePath,
mountPath: "",
content: mount.content,
serviceId: compose.composeId,
serviceType: "compose",
type: "file",
});
}
}
// Create domains
if (processedTemplate.domains && processedTemplate.domains.length > 0) {
for (const domain of processedTemplate.domains) {
await createDomain({
...domain,
domainType: "compose",
certificateType: "none",
composeId: compose.composeId,
host: domain.host || "",
});
}
}
return {
success: true,
message: "Template imported successfully",
};
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `Error importing template: ${error instanceof Error ? error.message : error}`,
});
}
}),
});

View File

@@ -65,10 +65,15 @@ export const dockerRouter = createTRPCRouter({
z.object({
appName: z.string().min(1),
serverId: z.string().optional(),
type: z.enum(["standalone", "swarm"]),
}),
)
.query(async ({ input }) => {
return await getContainersByAppLabel(input.appName, input.serverId);
return await getContainersByAppLabel(
input.appName,
input.type,
input.serverId,
);
}),
getStackContainersByAppName: protectedProcedure

View File

@@ -8,6 +8,8 @@ import {
apiSaveEnvironmentVariablesMariaDB,
apiSaveExternalPortMariaDB,
apiUpdateMariaDB,
apiRebuildMariadb,
mariadb as mariadbTable,
} from "@/server/db/schema";
import { cancelJobs } from "@/server/utils/backup";
import {
@@ -30,7 +32,10 @@ import {
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { observable } from "@trpc/server/observable";
import { z } from "zod";
import { eq } from "drizzle-orm";
import { db } from "@/server/db";
import { rebuildDatabase } from "@dokploy/server";
export const mariadbRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateMariaDB)
@@ -320,6 +325,63 @@ export const mariadbRouter = createTRPCRouter({
});
}
return true;
}),
move: protectedProcedure
.input(
z.object({
mariadbId: z.string(),
targetProjectId: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const mariadb = await findMariadbById(input.mariadbId);
if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this mariadb",
});
}
const targetProject = await findProjectById(input.targetProjectId);
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this project",
});
}
// Update the mariadb's projectId
const updatedMariadb = await db
.update(mariadbTable)
.set({
projectId: input.targetProjectId,
})
.where(eq(mariadbTable.mariadbId, input.mariadbId))
.returning()
.then((res) => res[0]);
if (!updatedMariadb) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to move mariadb",
});
}
return updatedMariadb;
}),
rebuild: protectedProcedure
.input(apiRebuildMariadb)
.mutation(async ({ input, ctx }) => {
const mariadb = await findMariadbById(input.mariadbId);
if (mariadb.project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to rebuild this MariaDB database",
});
}
await rebuildDatabase(mariadb.mariadbId, "mariadb");
return true;
}),
});

View File

@@ -4,10 +4,12 @@ import {
apiCreateMongo,
apiDeployMongo,
apiFindOneMongo,
apiRebuildMongo,
apiResetMongo,
apiSaveEnvironmentVariablesMongo,
apiSaveExternalPortMongo,
apiUpdateMongo,
mongo as mongoTable,
} from "@/server/db/schema";
import { cancelJobs } from "@/server/utils/backup";
import {
@@ -30,7 +32,10 @@ import {
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { observable } from "@trpc/server/observable";
import { z } from "zod";
import { eq } from "drizzle-orm";
import { db } from "@/server/db";
import { rebuildDatabase } from "@dokploy/server";
export const mongoRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateMongo)
@@ -334,6 +339,64 @@ export const mongoRouter = createTRPCRouter({
});
}
return true;
}),
move: protectedProcedure
.input(
z.object({
mongoId: z.string(),
targetProjectId: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const mongo = await findMongoById(input.mongoId);
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this mongo",
});
}
const targetProject = await findProjectById(input.targetProjectId);
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this project",
});
}
// Update the mongo's projectId
const updatedMongo = await db
.update(mongoTable)
.set({
projectId: input.targetProjectId,
})
.where(eq(mongoTable.mongoId, input.mongoId))
.returning()
.then((res) => res[0]);
if (!updatedMongo) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to move mongo",
});
}
return updatedMongo;
}),
rebuild: protectedProcedure
.input(apiRebuildMongo)
.mutation(async ({ input, ctx }) => {
const mongo = await findMongoById(input.mongoId);
if (mongo.project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to rebuild this MongoDB database",
});
}
await rebuildDatabase(mongo.mongoId, "mongo");
return true;
}),
});

View File

@@ -4,10 +4,12 @@ import {
apiCreateMySql,
apiDeployMySql,
apiFindOneMySql,
apiRebuildMysql,
apiResetMysql,
apiSaveEnvironmentVariablesMySql,
apiSaveExternalPortMySql,
apiUpdateMySql,
mysql as mysqlTable,
} from "@/server/db/schema";
import { TRPCError } from "@trpc/server";
@@ -23,6 +25,7 @@ import {
findBackupsByDbId,
findMySqlById,
findProjectById,
rebuildDatabase,
removeMySqlById,
removeService,
startService,
@@ -32,6 +35,9 @@ import {
updateMySqlById,
} from "@dokploy/server";
import { observable } from "@trpc/server/observable";
import { eq } from "drizzle-orm";
import { db } from "@/server/db";
import { z } from "zod";
export const mysqlRouter = createTRPCRouter({
create: protectedProcedure
@@ -330,6 +336,64 @@ export const mysqlRouter = createTRPCRouter({
});
}
return true;
}),
move: protectedProcedure
.input(
z.object({
mysqlId: z.string(),
targetProjectId: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const mysql = await findMySqlById(input.mysqlId);
if (mysql.project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this mysql",
});
}
const targetProject = await findProjectById(input.targetProjectId);
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this project",
});
}
// Update the mysql's projectId
const updatedMysql = await db
.update(mysqlTable)
.set({
projectId: input.targetProjectId,
})
.where(eq(mysqlTable.mysqlId, input.mysqlId))
.returning()
.then((res) => res[0]);
if (!updatedMysql) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to move mysql",
});
}
return updatedMysql;
}),
rebuild: protectedProcedure
.input(apiRebuildMysql)
.mutation(async ({ input, ctx }) => {
const mysql = await findMySqlById(input.mysqlId);
if (mysql.project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to rebuild this MySQL database",
});
}
await rebuildDatabase(mysql.mysqlId, "mysql");
return true;
}),
});

View File

@@ -4,10 +4,12 @@ import {
apiCreatePostgres,
apiDeployPostgres,
apiFindOnePostgres,
apiRebuildPostgres,
apiResetPostgres,
apiSaveEnvironmentVariablesPostgres,
apiSaveExternalPortPostgres,
apiUpdatePostgres,
postgres as postgresTable,
} from "@/server/db/schema";
import { cancelJobs } from "@/server/utils/backup";
import {
@@ -20,6 +22,7 @@ import {
findBackupsByDbId,
findPostgresById,
findProjectById,
rebuildDatabase,
removePostgresById,
removeService,
startService,
@@ -30,7 +33,9 @@ import {
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { observable } from "@trpc/server/observable";
import { z } from "zod";
import { eq } from "drizzle-orm";
import { db } from "@/server/db";
export const postgresRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreatePostgres)
@@ -350,6 +355,68 @@ export const postgresRouter = createTRPCRouter({
});
}
return true;
}),
move: protectedProcedure
.input(
z.object({
postgresId: z.string(),
targetProjectId: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const postgres = await findPostgresById(input.postgresId);
if (
postgres.project.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this postgres",
});
}
const targetProject = await findProjectById(input.targetProjectId);
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this project",
});
}
// Update the postgres's projectId
const updatedPostgres = await db
.update(postgresTable)
.set({
projectId: input.targetProjectId,
})
.where(eq(postgresTable.postgresId, input.postgresId))
.returning()
.then((res) => res[0]);
if (!updatedPostgres) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to move postgres",
});
}
return updatedPostgres;
}),
rebuild: protectedProcedure
.input(apiRebuildPostgres)
.mutation(async ({ input, ctx }) => {
const postgres = await findPostgresById(input.postgresId);
if (
postgres.project.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to rebuild this Postgres database",
});
}
await rebuildDatabase(postgres.postgresId, "postgres");
return true;
}),
});

View File

@@ -8,6 +8,8 @@ import {
apiSaveEnvironmentVariablesRedis,
apiSaveExternalPortRedis,
apiUpdateRedis,
redis as redisTable,
apiRebuildRedis,
} from "@/server/db/schema";
import { TRPCError } from "@trpc/server";
@@ -30,7 +32,10 @@ import {
updateRedisById,
} from "@dokploy/server";
import { observable } from "@trpc/server/observable";
import { eq } from "drizzle-orm";
import { db } from "@/server/db";
import { z } from "zod";
import { rebuildDatabase } from "@dokploy/server";
export const redisRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateRedis)
@@ -314,6 +319,63 @@ export const redisRouter = createTRPCRouter({
});
}
return true;
}),
move: protectedProcedure
.input(
z.object({
redisId: z.string(),
targetProjectId: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const redis = await findRedisById(input.redisId);
if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this redis",
});
}
const targetProject = await findProjectById(input.targetProjectId);
if (targetProject.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this project",
});
}
// Update the redis's projectId
const updatedRedis = await db
.update(redisTable)
.set({
projectId: input.targetProjectId,
})
.where(eq(redisTable.redisId, input.redisId))
.returning()
.then((res) => res[0]);
if (!updatedRedis) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to move redis",
});
}
return updatedRedis;
}),
rebuild: protectedProcedure
.input(apiRebuildRedis)
.mutation(async ({ input, ctx }) => {
const redis = await findRedisById(input.redisId);
if (redis.project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to rebuild this Redis database",
});
}
await rebuildDatabase(redis.redisId, "redis");
return true;
}),
});

View File

@@ -28,7 +28,6 @@ import {
getDokployImageTag,
getUpdateData,
initializeTraefik,
logRotationManager,
parseRawConfig,
paths,
prepareEnvironmentVariables,
@@ -42,10 +41,6 @@ import {
recreateDirectory,
sendDockerCleanupNotifications,
spawnAsync,
startService,
startServiceRemote,
stopService,
stopServiceRemote,
updateLetsEncryptEmail,
updateServerById,
updateServerTraefik,
@@ -53,6 +48,9 @@ import {
writeConfig,
writeMainConfig,
writeTraefikConfigInPath,
startLogCleanup,
stopLogCleanup,
getLogCleanupStatus,
} from "@dokploy/server";
import { checkGPUStatus, setupGPUSupport } from "@dokploy/server";
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
@@ -86,11 +84,9 @@ export const settingsRouter = createTRPCRouter({
.mutation(async ({ input }) => {
try {
if (input?.serverId) {
await stopServiceRemote(input.serverId, "dokploy-traefik");
await startServiceRemote(input.serverId, "dokploy-traefik");
await execAsync("docker restart dokploy-traefik");
} else if (!IS_CLOUD) {
await stopService("dokploy-traefik");
await startService("dokploy-traefik");
await execAsync("docker restart dokploy-traefik");
}
} catch (err) {
console.error(err);
@@ -101,13 +97,20 @@ export const settingsRouter = createTRPCRouter({
toggleDashboard: adminProcedure
.input(apiEnableDashboard)
.mutation(async ({ input }) => {
const ports = (await getTraefikPorts(input.serverId)).filter(
(port) =>
port.targetPort !== 80 &&
port.targetPort !== 443 &&
port.targetPort !== 8080,
);
await initializeTraefik({
additionalPorts: ports,
enableDashboard: input.enableDashboard,
serverId: input.serverId,
force: true,
});
return true;
}),
cleanUnusedImages: adminProcedure
.input(apiServerSchema)
.mutation(async ({ input }) => {
@@ -512,16 +515,18 @@ export const settingsRouter = createTRPCRouter({
.input(apiServerSchema)
.query(async ({ input }) => {
const command =
"docker service inspect --format='{{range .Spec.TaskTemplate.ContainerSpec.Env}}{{println .}}{{end}}' dokploy-traefik";
"docker container inspect dokploy-traefik --format '{{json .Config.Env}}'";
let result = "";
if (input?.serverId) {
const result = await execAsyncRemote(input.serverId, command);
return result.stdout.trim();
}
if (!IS_CLOUD) {
const result = await execAsync(command);
return result.stdout.trim();
const execResult = await execAsyncRemote(input.serverId, command);
result = execResult.stdout;
} else {
const execResult = await execAsync(command);
result = execResult.stdout;
}
const envVars = JSON.parse(result.trim());
return envVars.join("\n");
}),
writeTraefikEnv: adminProcedure
@@ -531,6 +536,7 @@ export const settingsRouter = createTRPCRouter({
await initializeTraefik({
env: envs,
serverId: input.serverId,
force: true,
});
return true;
@@ -538,27 +544,22 @@ export const settingsRouter = createTRPCRouter({
haveTraefikDashboardPortEnabled: adminProcedure
.input(apiServerSchema)
.query(async ({ input }) => {
const command = `docker service inspect --format='{{json .Endpoint.Ports}}' dokploy-traefik`;
const command = `docker container inspect --format='{{json .NetworkSettings.Ports}}' dokploy-traefik`;
let stdout = "";
if (input?.serverId) {
const result = await execAsyncRemote(input.serverId, command);
stdout = result.stdout;
} else if (!IS_CLOUD) {
const result = await execAsync(
"docker service inspect --format='{{json .Endpoint.Ports}}' dokploy-traefik",
);
const result = await execAsync(command);
stdout = result.stdout;
}
const parsed: any[] = JSON.parse(stdout.trim());
for (const port of parsed) {
if (port.PublishedPort === 8080) {
return true;
}
}
return false;
const ports = JSON.parse(stdout.trim());
return Object.entries(ports).some(([containerPort, bindings]) => {
const [port] = containerPort.split("/");
return port === "8080" && bindings && (bindings as any[]).length > 0;
});
}),
readStatsLogs: adminProcedure
@@ -578,48 +579,51 @@ export const settingsRouter = createTRPCRouter({
totalCount: 0,
};
}
const rawConfig = readMonitoringConfig();
const rawConfig = readMonitoringConfig(
!!input.dateRange?.start && !!input.dateRange?.end,
);
const parsedConfig = parseRawConfig(
rawConfig as string,
input.page,
input.sort,
input.search,
input.status,
input.dateRange,
);
return parsedConfig;
}),
readStats: adminProcedure.query(() => {
if (IS_CLOUD) {
return [];
}
const rawConfig = readMonitoringConfig();
const processedLogs = processLogs(rawConfig as string);
return processedLogs || [];
}),
getLogRotateStatus: adminProcedure.query(async () => {
if (IS_CLOUD) {
return true;
}
return await logRotationManager.getStatus();
}),
toggleLogRotate: adminProcedure
readStats: adminProcedure
.meta({
openapi: {
path: "/read-stats",
method: "POST",
override: true,
enabled: false,
},
})
.input(
z.object({
enable: z.boolean(),
}),
z
.object({
dateRange: z
.object({
start: z.string().optional(),
end: z.string().optional(),
})
.optional(),
})
.optional(),
)
.mutation(async ({ input }) => {
.query(({ input }) => {
if (IS_CLOUD) {
return true;
return [];
}
if (input.enable) {
await logRotationManager.activate();
} else {
await logRotationManager.deactivate();
}
return true;
const rawConfig = readMonitoringConfig(
!!input?.dateRange?.start || !!input?.dateRange?.end,
);
const processedLogs = processLogs(rawConfig as string, input?.dateRange);
return processedLogs || [];
}),
haveActivateRequests: adminProcedure.query(async () => {
if (IS_CLOUD) {
@@ -752,7 +756,6 @@ export const settingsRouter = createTRPCRouter({
z.object({
targetPort: z.number(),
publishedPort: z.number(),
publishMode: z.enum(["ingress", "host"]).default("host"),
}),
),
}),
@@ -768,6 +771,7 @@ export const settingsRouter = createTRPCRouter({
await initializeTraefik({
serverId: input.serverId,
additionalPorts: input.additionalPorts,
force: true,
});
return true;
} catch (error) {
@@ -784,47 +788,75 @@ export const settingsRouter = createTRPCRouter({
getTraefikPorts: adminProcedure
.input(apiServerSchema)
.query(async ({ input }) => {
const command = `docker service inspect --format='{{json .Endpoint.Ports}}' dokploy-traefik`;
return await getTraefikPorts(input?.serverId);
}),
updateLogCleanup: adminProcedure
.input(
z.object({
cronExpression: z.string().nullable(),
}),
)
.mutation(async ({ input }) => {
if (input.cronExpression) {
return startLogCleanup(input.cronExpression);
}
return stopLogCleanup();
}),
try {
let stdout = "";
if (input?.serverId) {
const result = await execAsyncRemote(input.serverId, command);
stdout = result.stdout;
} else if (!IS_CLOUD) {
const result = await execAsync(command);
stdout = result.stdout;
}
getLogCleanupStatus: adminProcedure.query(async () => {
return getLogCleanupStatus();
}),
});
const ports: {
Protocol: string;
TargetPort: number;
PublishedPort: number;
PublishMode: string;
}[] = JSON.parse(stdout.trim());
export const getTraefikPorts = async (serverId?: string) => {
const command = `docker container inspect --format='{{json .NetworkSettings.Ports}}' dokploy-traefik`;
try {
let stdout = "";
if (serverId) {
const result = await execAsyncRemote(serverId, command);
stdout = result.stdout;
} else if (!IS_CLOUD) {
const result = await execAsync(command);
stdout = result.stdout;
}
// Filter out the default ports (80, 443, and optionally 8080)
const additionalPorts = ports
.filter((port) => ![80, 443, 8080].includes(port.PublishedPort))
.map((port) => ({
targetPort: port.TargetPort,
publishedPort: port.PublishedPort,
publishMode: port.PublishMode.toLowerCase() as "host" | "ingress",
}));
const portsMap = JSON.parse(stdout.trim());
const additionalPorts: Array<{
targetPort: number;
publishedPort: number;
}> = [];
return additionalPorts;
} catch (error) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to get Traefik ports",
cause: error,
// Convert the Docker container port format to our expected format
for (const [containerPort, bindings] of Object.entries(portsMap)) {
if (!bindings) continue;
const [port = ""] = containerPort.split("/");
if (!port) continue;
const targetPortNum = Number.parseInt(port, 10);
if (Number.isNaN(targetPortNum)) continue;
// Skip default ports
if ([80, 443].includes(targetPortNum)) continue;
for (const binding of bindings as Array<{ HostPort: string }>) {
if (!binding.HostPort) continue;
const publishedPort = Number.parseInt(binding.HostPort, 10);
if (Number.isNaN(publishedPort)) continue;
additionalPorts.push({
targetPort: targetPortNum,
publishedPort,
});
}
}),
});
// {
// "Parallelism": 1,
// "Delay": 10000000000,
// "FailureAction": "rollback",
// "Order": "start-first"
// }
}
return additionalPorts;
} catch (error) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to get Traefik ports",
cause: error,
});
}
};