Merge branch 'canary' into hehehai/feat-server-custom-name

This commit is contained in:
Mauricio Siu
2024-06-08 14:37:51 -06:00
295 changed files with 43807 additions and 3328 deletions

View File

@@ -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();

View File

@@ -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",

View 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`;
}),
});

View 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;
}),
});

View File

@@ -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);
}),
});

View File

@@ -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;
}),
});

View File

@@ -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),
});

View 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;
}),
});

View File

@@ -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