mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
refactor(server): remove files
This commit is contained in:
@@ -1,151 +0,0 @@
|
|||||||
import { randomBytes } from "node:crypto";
|
|
||||||
import { db } from "@/server/db";
|
|
||||||
import {
|
|
||||||
admins,
|
|
||||||
type apiCreateUserInvitation,
|
|
||||||
auth,
|
|
||||||
users,
|
|
||||||
} from "@/server/db/schema";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import * as bcrypt from "bcrypt";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
|
|
||||||
export type Admin = typeof admins.$inferSelect;
|
|
||||||
export const createInvitation = async (
|
|
||||||
input: typeof apiCreateUserInvitation._type,
|
|
||||||
) => {
|
|
||||||
await db.transaction(async (tx) => {
|
|
||||||
const admin = await findAdmin();
|
|
||||||
|
|
||||||
const result = await tx
|
|
||||||
.insert(auth)
|
|
||||||
.values({
|
|
||||||
email: input.email,
|
|
||||||
rol: "user",
|
|
||||||
password: bcrypt.hashSync("01231203012312", 10),
|
|
||||||
})
|
|
||||||
.returning()
|
|
||||||
.then((res) => res[0]);
|
|
||||||
|
|
||||||
if (!result) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error to create the user",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const expiresIn24Hours = new Date();
|
|
||||||
expiresIn24Hours.setDate(expiresIn24Hours.getDate() + 1);
|
|
||||||
const token = randomBytes(32).toString("hex");
|
|
||||||
await tx
|
|
||||||
.insert(users)
|
|
||||||
.values({
|
|
||||||
adminId: admin.adminId,
|
|
||||||
authId: result.id,
|
|
||||||
token,
|
|
||||||
expirationDate: expiresIn24Hours.toISOString(),
|
|
||||||
})
|
|
||||||
.returning();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findAdminById = async (adminId: string) => {
|
|
||||||
const admin = await db.query.admins.findFirst({
|
|
||||||
where: eq(admins.adminId, adminId),
|
|
||||||
});
|
|
||||||
if (!admin) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Admin not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return admin;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateAdmin = async (
|
|
||||||
authId: string,
|
|
||||||
adminData: Partial<Admin>,
|
|
||||||
) => {
|
|
||||||
const admin = await db
|
|
||||||
.update(admins)
|
|
||||||
.set({
|
|
||||||
...adminData,
|
|
||||||
})
|
|
||||||
.where(eq(admins.authId, authId))
|
|
||||||
.returning()
|
|
||||||
.then((res) => res[0]);
|
|
||||||
|
|
||||||
return admin;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isAdminPresent = async () => {
|
|
||||||
const admin = await db.query.admins.findFirst();
|
|
||||||
if (!admin) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findAdminByAuthId = async (authId: string) => {
|
|
||||||
const admin = await db.query.admins.findFirst({
|
|
||||||
where: eq(admins.authId, authId),
|
|
||||||
});
|
|
||||||
if (!admin) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Admin not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return admin;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findAdmin = async () => {
|
|
||||||
const admin = await db.query.admins.findFirst({});
|
|
||||||
if (!admin) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Admin not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return admin;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getUserByToken = async (token: string) => {
|
|
||||||
const user = await db.query.users.findFirst({
|
|
||||||
where: eq(users.token, token),
|
|
||||||
with: {
|
|
||||||
auth: {
|
|
||||||
columns: {
|
|
||||||
password: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Invitation not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...user,
|
|
||||||
isExpired: user.isRegistered,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const removeUserByAuthId = async (authId: string) => {
|
|
||||||
await db
|
|
||||||
.delete(auth)
|
|
||||||
.where(eq(auth.id, authId))
|
|
||||||
.returning()
|
|
||||||
.then((res) => res[0]);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getDokployUrl = async () => {
|
|
||||||
const admin = await findAdmin();
|
|
||||||
|
|
||||||
if (admin.host) {
|
|
||||||
return `https://${admin.host}`;
|
|
||||||
}
|
|
||||||
return `http://${admin.serverIp}:${process.env.PORT}`;
|
|
||||||
};
|
|
||||||
@@ -1,394 +0,0 @@
|
|||||||
import { docker } from "@/server/constants";
|
|
||||||
import { db } from "@/server/db";
|
|
||||||
import { type apiCreateApplication, applications } from "@/server/db/schema";
|
|
||||||
import { generateAppName } from "@/server/db/schema";
|
|
||||||
import { getAdvancedStats } from "@/server/monitoring/utilts";
|
|
||||||
import {
|
|
||||||
buildApplication,
|
|
||||||
getBuildCommand,
|
|
||||||
mechanizeDockerContainer,
|
|
||||||
} from "@/server/utils/builders";
|
|
||||||
import { sendBuildErrorNotifications } from "@/server/utils/notifications/build-error";
|
|
||||||
import { sendBuildSuccessNotifications } from "@/server/utils/notifications/build-success";
|
|
||||||
import { execAsyncRemote } from "@/server/utils/process/execAsync";
|
|
||||||
import {
|
|
||||||
cloneBitbucketRepository,
|
|
||||||
getBitbucketCloneCommand,
|
|
||||||
} from "@/server/utils/providers/bitbucket";
|
|
||||||
import {
|
|
||||||
buildDocker,
|
|
||||||
buildRemoteDocker,
|
|
||||||
} from "@/server/utils/providers/docker";
|
|
||||||
import {
|
|
||||||
cloneGitRepository,
|
|
||||||
getCustomGitCloneCommand,
|
|
||||||
} from "@/server/utils/providers/git";
|
|
||||||
import {
|
|
||||||
cloneGithubRepository,
|
|
||||||
getGithubCloneCommand,
|
|
||||||
} from "@/server/utils/providers/github";
|
|
||||||
import {
|
|
||||||
cloneGitlabRepository,
|
|
||||||
getGitlabCloneCommand,
|
|
||||||
} from "@/server/utils/providers/gitlab";
|
|
||||||
import { createTraefikConfig } from "@/server/utils/traefik/application";
|
|
||||||
import { generatePassword } from "@/templates/utils";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import { getDokployUrl } from "./admin";
|
|
||||||
import { createDeployment, updateDeploymentStatus } from "./deployment";
|
|
||||||
import { validUniqueServerAppName } from "./project";
|
|
||||||
export type Application = typeof applications.$inferSelect;
|
|
||||||
|
|
||||||
export const createApplication = async (
|
|
||||||
input: typeof apiCreateApplication._type,
|
|
||||||
) => {
|
|
||||||
input.appName =
|
|
||||||
`${input.appName}-${generatePassword(6)}` || generateAppName("app");
|
|
||||||
if (input.appName) {
|
|
||||||
const valid = await validUniqueServerAppName(input.appName);
|
|
||||||
|
|
||||||
if (!valid) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "CONFLICT",
|
|
||||||
message: "Application with this 'AppName' already exists",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return await db.transaction(async (tx) => {
|
|
||||||
const newApplication = await tx
|
|
||||||
.insert(applications)
|
|
||||||
.values({
|
|
||||||
...input,
|
|
||||||
})
|
|
||||||
.returning()
|
|
||||||
.then((value) => value[0]);
|
|
||||||
|
|
||||||
if (!newApplication) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error to create the application",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV === "development") {
|
|
||||||
createTraefikConfig(newApplication.appName);
|
|
||||||
}
|
|
||||||
|
|
||||||
return newApplication;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findApplicationById = async (applicationId: string) => {
|
|
||||||
const application = await db.query.applications.findFirst({
|
|
||||||
where: eq(applications.applicationId, applicationId),
|
|
||||||
with: {
|
|
||||||
project: true,
|
|
||||||
domains: true,
|
|
||||||
deployments: true,
|
|
||||||
mounts: true,
|
|
||||||
redirects: true,
|
|
||||||
security: true,
|
|
||||||
ports: true,
|
|
||||||
registry: true,
|
|
||||||
gitlab: true,
|
|
||||||
github: true,
|
|
||||||
bitbucket: true,
|
|
||||||
server: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!application) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Application not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return application;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findApplicationByName = async (appName: string) => {
|
|
||||||
const application = await db.query.applications.findFirst({
|
|
||||||
where: eq(applications.appName, appName),
|
|
||||||
});
|
|
||||||
|
|
||||||
return application;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateApplication = async (
|
|
||||||
applicationId: string,
|
|
||||||
applicationData: Partial<Application>,
|
|
||||||
) => {
|
|
||||||
const application = await db
|
|
||||||
.update(applications)
|
|
||||||
.set({
|
|
||||||
...applicationData,
|
|
||||||
})
|
|
||||||
.where(eq(applications.applicationId, applicationId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
return application[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateApplicationStatus = async (
|
|
||||||
applicationId: string,
|
|
||||||
applicationStatus: Application["applicationStatus"],
|
|
||||||
) => {
|
|
||||||
const application = await db
|
|
||||||
.update(applications)
|
|
||||||
.set({
|
|
||||||
applicationStatus: applicationStatus,
|
|
||||||
})
|
|
||||||
.where(eq(applications.applicationId, applicationId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
return application;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deployApplication = async ({
|
|
||||||
applicationId,
|
|
||||||
titleLog = "Manual deployment",
|
|
||||||
descriptionLog = "",
|
|
||||||
}: {
|
|
||||||
applicationId: string;
|
|
||||||
titleLog: string;
|
|
||||||
descriptionLog: string;
|
|
||||||
}) => {
|
|
||||||
const application = await findApplicationById(applicationId);
|
|
||||||
const buildLink = `${await getDokployUrl()}/dashboard/project/${application.projectId}/services/application/${application.applicationId}?tab=deployments`;
|
|
||||||
const deployment = await createDeployment({
|
|
||||||
applicationId: applicationId,
|
|
||||||
title: titleLog,
|
|
||||||
description: descriptionLog,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (application.sourceType === "github") {
|
|
||||||
await cloneGithubRepository(application, deployment.logPath);
|
|
||||||
await buildApplication(application, deployment.logPath);
|
|
||||||
} else if (application.sourceType === "gitlab") {
|
|
||||||
await cloneGitlabRepository(application, deployment.logPath);
|
|
||||||
await buildApplication(application, deployment.logPath);
|
|
||||||
} else if (application.sourceType === "bitbucket") {
|
|
||||||
await cloneBitbucketRepository(application, deployment.logPath);
|
|
||||||
await buildApplication(application, deployment.logPath);
|
|
||||||
} else if (application.sourceType === "docker") {
|
|
||||||
await buildDocker(application, deployment.logPath);
|
|
||||||
} else if (application.sourceType === "git") {
|
|
||||||
await cloneGitRepository(application, deployment.logPath);
|
|
||||||
await buildApplication(application, deployment.logPath);
|
|
||||||
} else if (application.sourceType === "drop") {
|
|
||||||
await buildApplication(application, deployment.logPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
|
||||||
await updateApplicationStatus(applicationId, "done");
|
|
||||||
|
|
||||||
await sendBuildSuccessNotifications({
|
|
||||||
projectName: application.project.name,
|
|
||||||
applicationName: application.name,
|
|
||||||
applicationType: "application",
|
|
||||||
buildLink,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
|
||||||
await updateApplicationStatus(applicationId, "error");
|
|
||||||
await sendBuildErrorNotifications({
|
|
||||||
projectName: application.project.name,
|
|
||||||
applicationName: application.name,
|
|
||||||
applicationType: "application",
|
|
||||||
// @ts-ignore
|
|
||||||
errorMessage: error?.message || "Error to build",
|
|
||||||
buildLink,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
"Error on ",
|
|
||||||
application.buildType,
|
|
||||||
"/",
|
|
||||||
application.sourceType,
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const rebuildApplication = async ({
|
|
||||||
applicationId,
|
|
||||||
titleLog = "Rebuild deployment",
|
|
||||||
descriptionLog = "",
|
|
||||||
}: {
|
|
||||||
applicationId: string;
|
|
||||||
titleLog: string;
|
|
||||||
descriptionLog: string;
|
|
||||||
}) => {
|
|
||||||
const application = await findApplicationById(applicationId);
|
|
||||||
const deployment = await createDeployment({
|
|
||||||
applicationId: applicationId,
|
|
||||||
title: titleLog,
|
|
||||||
description: descriptionLog,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (application.sourceType === "github") {
|
|
||||||
await buildApplication(application, deployment.logPath);
|
|
||||||
} else if (application.sourceType === "gitlab") {
|
|
||||||
await buildApplication(application, deployment.logPath);
|
|
||||||
} else if (application.sourceType === "bitbucket") {
|
|
||||||
await buildApplication(application, deployment.logPath);
|
|
||||||
} else if (application.sourceType === "docker") {
|
|
||||||
await buildDocker(application, deployment.logPath);
|
|
||||||
} else if (application.sourceType === "git") {
|
|
||||||
await buildApplication(application, deployment.logPath);
|
|
||||||
} else if (application.sourceType === "drop") {
|
|
||||||
await buildApplication(application, deployment.logPath);
|
|
||||||
}
|
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
|
||||||
await updateApplicationStatus(applicationId, "done");
|
|
||||||
} catch (error) {
|
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
|
||||||
await updateApplicationStatus(applicationId, "error");
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deployRemoteApplication = async ({
|
|
||||||
applicationId,
|
|
||||||
titleLog = "Manual deployment",
|
|
||||||
descriptionLog = "",
|
|
||||||
}: {
|
|
||||||
applicationId: string;
|
|
||||||
titleLog: string;
|
|
||||||
descriptionLog: string;
|
|
||||||
}) => {
|
|
||||||
const application = await findApplicationById(applicationId);
|
|
||||||
const buildLink = `${await getDokployUrl()}/dashboard/project/${application.projectId}/services/application/${application.applicationId}?tab=deployments`;
|
|
||||||
const deployment = await createDeployment({
|
|
||||||
applicationId: applicationId,
|
|
||||||
title: titleLog,
|
|
||||||
description: descriptionLog,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (application.serverId) {
|
|
||||||
let command = "set -e;";
|
|
||||||
if (application.sourceType === "github") {
|
|
||||||
command += await getGithubCloneCommand(application, deployment.logPath);
|
|
||||||
} else if (application.sourceType === "gitlab") {
|
|
||||||
command += await getGitlabCloneCommand(application, deployment.logPath);
|
|
||||||
} else if (application.sourceType === "bitbucket") {
|
|
||||||
command += await getBitbucketCloneCommand(
|
|
||||||
application,
|
|
||||||
deployment.logPath,
|
|
||||||
);
|
|
||||||
} else if (application.sourceType === "git") {
|
|
||||||
command += await getCustomGitCloneCommand(
|
|
||||||
application,
|
|
||||||
deployment.logPath,
|
|
||||||
);
|
|
||||||
} else if (application.sourceType === "docker") {
|
|
||||||
command += await buildRemoteDocker(application, deployment.logPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (application.sourceType !== "docker") {
|
|
||||||
command += getBuildCommand(application, deployment.logPath);
|
|
||||||
}
|
|
||||||
await execAsyncRemote(application.serverId, command);
|
|
||||||
await mechanizeDockerContainer(application);
|
|
||||||
}
|
|
||||||
|
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
|
||||||
await updateApplicationStatus(applicationId, "done");
|
|
||||||
|
|
||||||
await sendBuildSuccessNotifications({
|
|
||||||
projectName: application.project.name,
|
|
||||||
applicationName: application.name,
|
|
||||||
applicationType: "application",
|
|
||||||
buildLink,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
|
||||||
await updateApplicationStatus(applicationId, "error");
|
|
||||||
await sendBuildErrorNotifications({
|
|
||||||
projectName: application.project.name,
|
|
||||||
applicationName: application.name,
|
|
||||||
applicationType: "application",
|
|
||||||
// @ts-ignore
|
|
||||||
errorMessage: error?.message || "Error to build",
|
|
||||||
buildLink,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
"Error on ",
|
|
||||||
application.buildType,
|
|
||||||
"/",
|
|
||||||
application.sourceType,
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const rebuildRemoteApplication = async ({
|
|
||||||
applicationId,
|
|
||||||
titleLog = "Rebuild deployment",
|
|
||||||
descriptionLog = "",
|
|
||||||
}: {
|
|
||||||
applicationId: string;
|
|
||||||
titleLog: string;
|
|
||||||
descriptionLog: string;
|
|
||||||
}) => {
|
|
||||||
const application = await findApplicationById(applicationId);
|
|
||||||
const deployment = await createDeployment({
|
|
||||||
applicationId: applicationId,
|
|
||||||
title: titleLog,
|
|
||||||
description: descriptionLog,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (application.serverId) {
|
|
||||||
if (application.sourceType !== "docker") {
|
|
||||||
let command = "set -e;";
|
|
||||||
command += getBuildCommand(application, deployment.logPath);
|
|
||||||
await execAsyncRemote(application.serverId, command);
|
|
||||||
}
|
|
||||||
await mechanizeDockerContainer(application);
|
|
||||||
}
|
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
|
||||||
await updateApplicationStatus(applicationId, "done");
|
|
||||||
} catch (error) {
|
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
|
||||||
await updateApplicationStatus(applicationId, "error");
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getApplicationStats = async (appName: string) => {
|
|
||||||
const filter = {
|
|
||||||
status: ["running"],
|
|
||||||
label: [`com.docker.swarm.service.name=${appName}`],
|
|
||||||
};
|
|
||||||
|
|
||||||
const containers = await docker.listContainers({
|
|
||||||
filters: JSON.stringify(filter),
|
|
||||||
});
|
|
||||||
|
|
||||||
const container = containers[0];
|
|
||||||
if (!container || container?.State !== "running") {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await getAdvancedStats(appName);
|
|
||||||
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
import { randomBytes } from "node:crypto";
|
|
||||||
import { db } from "@/server/db";
|
|
||||||
import {
|
|
||||||
admins,
|
|
||||||
type apiCreateAdmin,
|
|
||||||
type apiCreateUser,
|
|
||||||
auth,
|
|
||||||
users,
|
|
||||||
} from "@/server/db/schema";
|
|
||||||
import { getPublicIpWithFallback } from "@/server/wss/terminal";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import * as bcrypt from "bcrypt";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import encode from "hi-base32";
|
|
||||||
import { TOTP } from "otpauth";
|
|
||||||
import QRCode from "qrcode";
|
|
||||||
|
|
||||||
export type Auth = typeof auth.$inferSelect;
|
|
||||||
|
|
||||||
export const createAdmin = async (input: typeof apiCreateAdmin._type) => {
|
|
||||||
return await db.transaction(async (tx) => {
|
|
||||||
const hashedPassword = bcrypt.hashSync(input.password, 10);
|
|
||||||
const newAuth = await tx
|
|
||||||
.insert(auth)
|
|
||||||
.values({
|
|
||||||
email: input.email,
|
|
||||||
password: hashedPassword,
|
|
||||||
rol: "admin",
|
|
||||||
})
|
|
||||||
.returning()
|
|
||||||
.then((res) => res[0]);
|
|
||||||
|
|
||||||
if (!newAuth) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error to create the user",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await tx
|
|
||||||
.insert(admins)
|
|
||||||
.values({
|
|
||||||
authId: newAuth.id,
|
|
||||||
serverIp: await getPublicIpWithFallback(),
|
|
||||||
})
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
return newAuth;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createUser = async (input: typeof apiCreateUser._type) => {
|
|
||||||
return await db.transaction(async (tx) => {
|
|
||||||
const hashedPassword = bcrypt.hashSync(input.password, 10);
|
|
||||||
const res = await tx
|
|
||||||
.update(auth)
|
|
||||||
.set({
|
|
||||||
password: hashedPassword,
|
|
||||||
})
|
|
||||||
.where(eq(auth.id, input.id))
|
|
||||||
.returning()
|
|
||||||
.then((res) => res[0]);
|
|
||||||
|
|
||||||
if (!res) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error to create the user",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await tx
|
|
||||||
.update(users)
|
|
||||||
.set({
|
|
||||||
isRegistered: true,
|
|
||||||
expirationDate: undefined,
|
|
||||||
})
|
|
||||||
.where(eq(users.token, input.token))
|
|
||||||
.returning()
|
|
||||||
.then((res) => res[0]);
|
|
||||||
|
|
||||||
return user;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findAuthByEmail = async (email: string) => {
|
|
||||||
const result = await db.query.auth.findFirst({
|
|
||||||
where: eq(auth.email, email),
|
|
||||||
});
|
|
||||||
if (!result) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Auth not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findAuthById = async (authId: string) => {
|
|
||||||
const result = await db.query.auth.findFirst({
|
|
||||||
where: eq(auth.id, authId),
|
|
||||||
columns: {
|
|
||||||
password: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!result) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Auth not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateAuthById = async (
|
|
||||||
authId: string,
|
|
||||||
authData: Partial<Auth>,
|
|
||||||
) => {
|
|
||||||
const result = await db
|
|
||||||
.update(auth)
|
|
||||||
.set({
|
|
||||||
...authData,
|
|
||||||
})
|
|
||||||
.where(eq(auth.id, authId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
return result[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const generate2FASecret = async (authId: string) => {
|
|
||||||
const auth = await findAuthById(authId);
|
|
||||||
|
|
||||||
const base32_secret = generateBase32Secret();
|
|
||||||
|
|
||||||
const totp = new TOTP({
|
|
||||||
issuer: "Dokploy",
|
|
||||||
label: `${auth?.email}`,
|
|
||||||
algorithm: "SHA1",
|
|
||||||
digits: 6,
|
|
||||||
secret: base32_secret,
|
|
||||||
});
|
|
||||||
|
|
||||||
const otpauth_url = totp.toString();
|
|
||||||
|
|
||||||
const qrUrl = await QRCode.toDataURL(otpauth_url);
|
|
||||||
|
|
||||||
return {
|
|
||||||
qrCodeUrl: qrUrl,
|
|
||||||
secret: base32_secret,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const verify2FA = async (
|
|
||||||
auth: Omit<Auth, "password">,
|
|
||||||
secret: string,
|
|
||||||
pin: string,
|
|
||||||
) => {
|
|
||||||
const totp = new TOTP({
|
|
||||||
issuer: "Dokploy",
|
|
||||||
label: `${auth?.email}`,
|
|
||||||
algorithm: "SHA1",
|
|
||||||
digits: 6,
|
|
||||||
secret: secret,
|
|
||||||
});
|
|
||||||
|
|
||||||
const delta = totp.validate({ token: pin });
|
|
||||||
|
|
||||||
if (delta === null) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Invalid 2FA code",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return auth;
|
|
||||||
};
|
|
||||||
|
|
||||||
const generateBase32Secret = () => {
|
|
||||||
const buffer = randomBytes(15);
|
|
||||||
const base32 = encode.encode(buffer).replace(/=/g, "").substring(0, 24);
|
|
||||||
return base32;
|
|
||||||
};
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
import { db } from "@/server/db";
|
|
||||||
import { type apiCreateBackup, backups } from "@/server/db/schema";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
|
|
||||||
export type Backup = typeof backups.$inferSelect;
|
|
||||||
|
|
||||||
export type BackupSchedule = Awaited<ReturnType<typeof findBackupById>>;
|
|
||||||
|
|
||||||
export const createBackup = async (input: typeof apiCreateBackup._type) => {
|
|
||||||
const newBackup = await db
|
|
||||||
.insert(backups)
|
|
||||||
.values({
|
|
||||||
...input,
|
|
||||||
})
|
|
||||||
.returning()
|
|
||||||
.then((value) => value[0]);
|
|
||||||
|
|
||||||
if (!newBackup) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error to create the Backup",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return newBackup;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findBackupById = async (backupId: string) => {
|
|
||||||
const backup = await db.query.backups.findFirst({
|
|
||||||
where: eq(backups.backupId, backupId),
|
|
||||||
with: {
|
|
||||||
postgres: true,
|
|
||||||
mysql: true,
|
|
||||||
mariadb: true,
|
|
||||||
mongo: true,
|
|
||||||
destination: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!backup) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Backup not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return backup;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateBackupById = async (
|
|
||||||
backupId: string,
|
|
||||||
backupData: Partial<Backup>,
|
|
||||||
) => {
|
|
||||||
const result = await db
|
|
||||||
.update(backups)
|
|
||||||
.set({
|
|
||||||
...backupData,
|
|
||||||
})
|
|
||||||
.where(eq(backups.backupId, backupId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
return result[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const removeBackupById = async (backupId: string) => {
|
|
||||||
const result = await db
|
|
||||||
.delete(backups)
|
|
||||||
.where(eq(backups.backupId, backupId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
return result[0];
|
|
||||||
};
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
import { db } from "@/server/db";
|
|
||||||
import {
|
|
||||||
type apiCreateBitbucket,
|
|
||||||
type apiUpdateBitbucket,
|
|
||||||
bitbucket,
|
|
||||||
gitProvider,
|
|
||||||
} from "@/server/db/schema";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
|
|
||||||
export type Bitbucket = typeof bitbucket.$inferSelect;
|
|
||||||
|
|
||||||
export const createBitbucket = async (
|
|
||||||
input: typeof apiCreateBitbucket._type,
|
|
||||||
) => {
|
|
||||||
return await db.transaction(async (tx) => {
|
|
||||||
const newGitProvider = await tx
|
|
||||||
.insert(gitProvider)
|
|
||||||
.values({
|
|
||||||
providerType: "bitbucket",
|
|
||||||
authId: input.authId,
|
|
||||||
name: input.name,
|
|
||||||
})
|
|
||||||
.returning()
|
|
||||||
.then((response) => response[0]);
|
|
||||||
|
|
||||||
if (!newGitProvider) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error to create the git provider",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await tx
|
|
||||||
.insert(bitbucket)
|
|
||||||
.values({
|
|
||||||
...input,
|
|
||||||
gitProviderId: newGitProvider?.gitProviderId,
|
|
||||||
})
|
|
||||||
.returning()
|
|
||||||
.then((response) => response[0]);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findBitbucketById = async (bitbucketId: string) => {
|
|
||||||
const bitbucketProviderResult = await db.query.bitbucket.findFirst({
|
|
||||||
where: eq(bitbucket.bitbucketId, bitbucketId),
|
|
||||||
with: {
|
|
||||||
gitProvider: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!bitbucketProviderResult) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Bitbucket Provider not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return bitbucketProviderResult;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateBitbucket = async (
|
|
||||||
bitbucketId: string,
|
|
||||||
input: typeof apiUpdateBitbucket._type,
|
|
||||||
) => {
|
|
||||||
return await db.transaction(async (tx) => {
|
|
||||||
const result = await tx
|
|
||||||
.update(bitbucket)
|
|
||||||
.set({
|
|
||||||
...input,
|
|
||||||
})
|
|
||||||
.where(eq(bitbucket.bitbucketId, bitbucketId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
if (input.name) {
|
|
||||||
await tx
|
|
||||||
.update(gitProvider)
|
|
||||||
.set({
|
|
||||||
name: input.name,
|
|
||||||
})
|
|
||||||
.where(eq(gitProvider.gitProviderId, input.gitProviderId))
|
|
||||||
.returning();
|
|
||||||
}
|
|
||||||
|
|
||||||
return result[0];
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
import fs from "node:fs";
|
|
||||||
import path from "node:path";
|
|
||||||
import { paths } from "@/server/constants";
|
|
||||||
import { db } from "@/server/db";
|
|
||||||
import { type apiCreateCertificate, certificates } from "@/server/db/schema";
|
|
||||||
import { removeDirectoryIfExistsContent } from "@/server/utils/filesystem/directory";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import { dump } from "js-yaml";
|
|
||||||
import type { z } from "zod";
|
|
||||||
|
|
||||||
export type Certificate = typeof certificates.$inferSelect;
|
|
||||||
|
|
||||||
export const findCertificateById = async (certificateId: string) => {
|
|
||||||
const certificate = await db.query.certificates.findFirst({
|
|
||||||
where: eq(certificates.certificateId, certificateId),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!certificate) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Certificate not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return certificate;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createCertificate = async (
|
|
||||||
certificateData: z.infer<typeof apiCreateCertificate>,
|
|
||||||
) => {
|
|
||||||
const certificate = await db
|
|
||||||
.insert(certificates)
|
|
||||||
.values({
|
|
||||||
...certificateData,
|
|
||||||
})
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
if (!certificate || certificate[0] === undefined) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Failed to create the certificate",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const cer = certificate[0];
|
|
||||||
|
|
||||||
createCertificateFiles(cer);
|
|
||||||
return cer;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const removeCertificateById = async (certificateId: string) => {
|
|
||||||
const { CERTIFICATES_PATH } = paths();
|
|
||||||
const certificate = await findCertificateById(certificateId);
|
|
||||||
const certDir = path.join(CERTIFICATES_PATH, certificate.certificatePath);
|
|
||||||
|
|
||||||
await removeDirectoryIfExistsContent(certDir);
|
|
||||||
const result = await db
|
|
||||||
.delete(certificates)
|
|
||||||
.where(eq(certificates.certificateId, certificateId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
if (!result) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Failed to delete the certificate",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findCertificates = async () => {
|
|
||||||
return await db.query.certificates.findMany();
|
|
||||||
};
|
|
||||||
|
|
||||||
const createCertificateFiles = (certificate: Certificate) => {
|
|
||||||
const { CERTIFICATES_PATH } = paths();
|
|
||||||
const dockerPath = "/etc/traefik";
|
|
||||||
const certDir = path.join(CERTIFICATES_PATH, certificate.certificatePath);
|
|
||||||
const crtPath = path.join(certDir, "chain.crt");
|
|
||||||
const keyPath = path.join(certDir, "privkey.key");
|
|
||||||
|
|
||||||
const chainPath = path.join(dockerPath, certDir, "chain.crt");
|
|
||||||
const keyPathDocker = path.join(dockerPath, certDir, "privkey.key");
|
|
||||||
|
|
||||||
if (!fs.existsSync(certDir)) {
|
|
||||||
fs.mkdirSync(certDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFileSync(crtPath, certificate.certificateData);
|
|
||||||
fs.writeFileSync(keyPath, certificate.privateKey);
|
|
||||||
|
|
||||||
const traefikConfig = {
|
|
||||||
tls: {
|
|
||||||
certificates: [
|
|
||||||
{
|
|
||||||
certFile: chainPath,
|
|
||||||
keyFile: keyPathDocker,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const yamlConfig = dump(traefikConfig);
|
|
||||||
const configFile = path.join(certDir, "certificate.yml");
|
|
||||||
fs.writeFileSync(configFile, yamlConfig);
|
|
||||||
};
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
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;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,467 +0,0 @@
|
|||||||
import { join } from "node:path";
|
|
||||||
import { paths } from "@/server/constants";
|
|
||||||
import { db } from "@/server/db";
|
|
||||||
import { type apiCreateCompose, compose } from "@/server/db/schema";
|
|
||||||
import { generateAppName } from "@/server/db/schema";
|
|
||||||
import {
|
|
||||||
buildCompose,
|
|
||||||
getBuildComposeCommand,
|
|
||||||
} from "@/server/utils/builders/compose";
|
|
||||||
import { randomizeSpecificationFile } from "@/server/utils/docker/compose";
|
|
||||||
import {
|
|
||||||
cloneCompose,
|
|
||||||
cloneComposeRemote,
|
|
||||||
loadDockerCompose,
|
|
||||||
loadDockerComposeRemote,
|
|
||||||
} from "@/server/utils/docker/domain";
|
|
||||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
|
||||||
import { sendBuildErrorNotifications } from "@/server/utils/notifications/build-error";
|
|
||||||
import { sendBuildSuccessNotifications } from "@/server/utils/notifications/build-success";
|
|
||||||
import { execAsync, execAsyncRemote } from "@/server/utils/process/execAsync";
|
|
||||||
import {
|
|
||||||
cloneBitbucketRepository,
|
|
||||||
getBitbucketCloneCommand,
|
|
||||||
} from "@/server/utils/providers/bitbucket";
|
|
||||||
import {
|
|
||||||
cloneGitRepository,
|
|
||||||
getCustomGitCloneCommand,
|
|
||||||
} from "@/server/utils/providers/git";
|
|
||||||
import {
|
|
||||||
cloneGithubRepository,
|
|
||||||
getGithubCloneCommand,
|
|
||||||
} from "@/server/utils/providers/github";
|
|
||||||
import {
|
|
||||||
cloneGitlabRepository,
|
|
||||||
getGitlabCloneCommand,
|
|
||||||
} from "@/server/utils/providers/gitlab";
|
|
||||||
import {
|
|
||||||
createComposeFile,
|
|
||||||
getCreateComposeFileCommand,
|
|
||||||
} from "@/server/utils/providers/raw";
|
|
||||||
import { generatePassword } from "@/templates/utils";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import { getDokployUrl } from "./admin";
|
|
||||||
import { createDeploymentCompose, updateDeploymentStatus } from "./deployment";
|
|
||||||
import { validUniqueServerAppName } from "./project";
|
|
||||||
|
|
||||||
export type Compose = typeof compose.$inferSelect;
|
|
||||||
|
|
||||||
export const createCompose = async (input: typeof apiCreateCompose._type) => {
|
|
||||||
input.appName =
|
|
||||||
`${input.appName}-${generatePassword(6)}` || generateAppName("compose");
|
|
||||||
if (input.appName) {
|
|
||||||
const valid = await validUniqueServerAppName(input.appName);
|
|
||||||
|
|
||||||
if (!valid) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "CONFLICT",
|
|
||||||
message: "Service with this 'AppName' already exists",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
) => {
|
|
||||||
if (input.appName) {
|
|
||||||
const valid = await validUniqueServerAppName(input.appName);
|
|
||||||
|
|
||||||
if (!valid) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "CONFLICT",
|
|
||||||
message: "Service with this 'AppName' already exists",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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,
|
|
||||||
domains: true,
|
|
||||||
github: true,
|
|
||||||
gitlab: true,
|
|
||||||
bitbucket: true,
|
|
||||||
server: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!result) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Compose not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const loadServices = async (
|
|
||||||
composeId: string,
|
|
||||||
type: "fetch" | "cache" = "fetch",
|
|
||||||
) => {
|
|
||||||
const compose = await findComposeById(composeId);
|
|
||||||
|
|
||||||
if (type === "fetch") {
|
|
||||||
if (compose.serverId) {
|
|
||||||
await cloneComposeRemote(compose);
|
|
||||||
} else {
|
|
||||||
await cloneCompose(compose);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let composeData: ComposeSpecification | null;
|
|
||||||
|
|
||||||
if (compose.serverId) {
|
|
||||||
composeData = await loadDockerComposeRemote(compose);
|
|
||||||
} else {
|
|
||||||
composeData = await loadDockerCompose(compose);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (compose.randomize && composeData) {
|
|
||||||
const randomizedCompose = randomizeSpecificationFile(
|
|
||||||
composeData,
|
|
||||||
compose.suffix,
|
|
||||||
);
|
|
||||||
composeData = randomizedCompose;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!composeData?.services) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Services not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const services = Object.keys(composeData.services);
|
|
||||||
|
|
||||||
return [...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",
|
|
||||||
descriptionLog = "",
|
|
||||||
}: {
|
|
||||||
composeId: string;
|
|
||||||
titleLog: string;
|
|
||||||
descriptionLog: string;
|
|
||||||
}) => {
|
|
||||||
const compose = await findComposeById(composeId);
|
|
||||||
const buildLink = `${await getDokployUrl()}/dashboard/project/${compose.projectId}/services/compose/${compose.composeId}?tab=deployments`;
|
|
||||||
const deployment = await createDeploymentCompose({
|
|
||||||
composeId: composeId,
|
|
||||||
title: titleLog,
|
|
||||||
description: descriptionLog,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (compose.sourceType === "github") {
|
|
||||||
await cloneGithubRepository(compose, deployment.logPath, true);
|
|
||||||
} else if (compose.sourceType === "gitlab") {
|
|
||||||
await cloneGitlabRepository(compose, deployment.logPath, true);
|
|
||||||
} else if (compose.sourceType === "bitbucket") {
|
|
||||||
await cloneBitbucketRepository(compose, deployment.logPath, true);
|
|
||||||
} else if (compose.sourceType === "git") {
|
|
||||||
await cloneGitRepository(compose, deployment.logPath, true);
|
|
||||||
} else if (compose.sourceType === "raw") {
|
|
||||||
await createComposeFile(compose, deployment.logPath);
|
|
||||||
}
|
|
||||||
await buildCompose(compose, deployment.logPath);
|
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
|
||||||
await updateCompose(composeId, {
|
|
||||||
composeStatus: "done",
|
|
||||||
});
|
|
||||||
|
|
||||||
await sendBuildSuccessNotifications({
|
|
||||||
projectName: compose.project.name,
|
|
||||||
applicationName: compose.name,
|
|
||||||
applicationType: "compose",
|
|
||||||
buildLink,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
|
||||||
await updateCompose(composeId, {
|
|
||||||
composeStatus: "error",
|
|
||||||
});
|
|
||||||
await sendBuildErrorNotifications({
|
|
||||||
projectName: compose.project.name,
|
|
||||||
applicationName: compose.name,
|
|
||||||
applicationType: "compose",
|
|
||||||
// @ts-ignore
|
|
||||||
errorMessage: error?.message || "Error to build",
|
|
||||||
buildLink,
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const rebuildCompose = async ({
|
|
||||||
composeId,
|
|
||||||
titleLog = "Rebuild deployment",
|
|
||||||
descriptionLog = "",
|
|
||||||
}: {
|
|
||||||
composeId: string;
|
|
||||||
titleLog: string;
|
|
||||||
descriptionLog: string;
|
|
||||||
}) => {
|
|
||||||
const compose = await findComposeById(composeId);
|
|
||||||
const deployment = await createDeploymentCompose({
|
|
||||||
composeId: composeId,
|
|
||||||
title: titleLog,
|
|
||||||
description: descriptionLog,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (compose.serverId) {
|
|
||||||
await getBuildComposeCommand(compose, deployment.logPath);
|
|
||||||
} else {
|
|
||||||
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 deployRemoteCompose = async ({
|
|
||||||
composeId,
|
|
||||||
titleLog = "Manual deployment",
|
|
||||||
descriptionLog = "",
|
|
||||||
}: {
|
|
||||||
composeId: string;
|
|
||||||
titleLog: string;
|
|
||||||
descriptionLog: string;
|
|
||||||
}) => {
|
|
||||||
const compose = await findComposeById(composeId);
|
|
||||||
const buildLink = `${await getDokployUrl()}/dashboard/project/${compose.projectId}/services/compose/${compose.composeId}?tab=deployments`;
|
|
||||||
const deployment = await createDeploymentCompose({
|
|
||||||
composeId: composeId,
|
|
||||||
title: titleLog,
|
|
||||||
description: descriptionLog,
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
if (compose.serverId) {
|
|
||||||
let command = "set -e;";
|
|
||||||
|
|
||||||
if (compose.sourceType === "github") {
|
|
||||||
command += await getGithubCloneCommand(
|
|
||||||
compose,
|
|
||||||
deployment.logPath,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
} else if (compose.sourceType === "gitlab") {
|
|
||||||
command += await getGitlabCloneCommand(
|
|
||||||
compose,
|
|
||||||
deployment.logPath,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
} else if (compose.sourceType === "bitbucket") {
|
|
||||||
command += await getBitbucketCloneCommand(
|
|
||||||
compose,
|
|
||||||
deployment.logPath,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
} else if (compose.sourceType === "git") {
|
|
||||||
command += await getCustomGitCloneCommand(
|
|
||||||
compose,
|
|
||||||
deployment.logPath,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
} else if (compose.sourceType === "raw") {
|
|
||||||
command += getCreateComposeFileCommand(compose, deployment.logPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
await execAsyncRemote(compose.serverId, command);
|
|
||||||
await getBuildComposeCommand(compose, deployment.logPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
|
||||||
await updateCompose(composeId, {
|
|
||||||
composeStatus: "done",
|
|
||||||
});
|
|
||||||
|
|
||||||
await sendBuildSuccessNotifications({
|
|
||||||
projectName: compose.project.name,
|
|
||||||
applicationName: compose.name,
|
|
||||||
applicationType: "compose",
|
|
||||||
buildLink,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
|
||||||
await updateCompose(composeId, {
|
|
||||||
composeStatus: "error",
|
|
||||||
});
|
|
||||||
await sendBuildErrorNotifications({
|
|
||||||
projectName: compose.project.name,
|
|
||||||
applicationName: compose.name,
|
|
||||||
applicationType: "compose",
|
|
||||||
// @ts-ignore
|
|
||||||
errorMessage: error?.message || "Error to build",
|
|
||||||
buildLink,
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const rebuildRemoteCompose = async ({
|
|
||||||
composeId,
|
|
||||||
titleLog = "Rebuild deployment",
|
|
||||||
descriptionLog = "",
|
|
||||||
}: {
|
|
||||||
composeId: string;
|
|
||||||
titleLog: string;
|
|
||||||
descriptionLog: string;
|
|
||||||
}) => {
|
|
||||||
const compose = await findComposeById(composeId);
|
|
||||||
const deployment = await createDeploymentCompose({
|
|
||||||
composeId: composeId,
|
|
||||||
title: titleLog,
|
|
||||||
description: descriptionLog,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (compose.serverId) {
|
|
||||||
await getBuildComposeCommand(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 { COMPOSE_PATH } = paths(!!compose.serverId);
|
|
||||||
const projectPath = join(COMPOSE_PATH, compose.appName);
|
|
||||||
|
|
||||||
if (compose.composeType === "stack") {
|
|
||||||
const command = `cd ${projectPath} && docker stack rm ${compose.appName} && rm -rf ${projectPath}`;
|
|
||||||
if (compose.serverId) {
|
|
||||||
await execAsyncRemote(compose.serverId, command);
|
|
||||||
} else {
|
|
||||||
await execAsync(command);
|
|
||||||
}
|
|
||||||
await execAsync(command, {
|
|
||||||
cwd: projectPath,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const command = `cd ${projectPath} && docker compose -p ${compose.appName} down && rm -rf ${projectPath}`;
|
|
||||||
if (compose.serverId) {
|
|
||||||
await execAsyncRemote(compose.serverId, command);
|
|
||||||
} else {
|
|
||||||
await execAsync(command, {
|
|
||||||
cwd: projectPath,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const stopCompose = async (composeId: string) => {
|
|
||||||
const compose = await findComposeById(composeId);
|
|
||||||
try {
|
|
||||||
const { COMPOSE_PATH } = paths(!!compose.serverId);
|
|
||||||
if (compose.composeType === "docker-compose") {
|
|
||||||
if (compose.serverId) {
|
|
||||||
await execAsyncRemote(
|
|
||||||
compose.serverId,
|
|
||||||
`cd ${join(COMPOSE_PATH, compose.appName)} && docker compose -p ${compose.appName} stop`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
@@ -1,372 +0,0 @@
|
|||||||
import { existsSync, promises as fsPromises } from "node:fs";
|
|
||||||
import path from "node:path";
|
|
||||||
import { paths } from "@/server/constants";
|
|
||||||
import { db } from "@/server/db";
|
|
||||||
import {
|
|
||||||
type apiCreateDeployment,
|
|
||||||
type apiCreateDeploymentCompose,
|
|
||||||
type apiCreateDeploymentServer,
|
|
||||||
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,
|
|
||||||
updateApplicationStatus,
|
|
||||||
} from "./application";
|
|
||||||
import { type Compose, findComposeById, updateCompose } from "./compose";
|
|
||||||
import { type Server, findServerById } from "./server";
|
|
||||||
|
|
||||||
import { execAsyncRemote } from "@/server/utils/process/execAsync";
|
|
||||||
|
|
||||||
export type Deployment = typeof deployments.$inferSelect;
|
|
||||||
|
|
||||||
export const findDeploymentById = async (applicationId: string) => {
|
|
||||||
const application = await db.query.deployments.findFirst({
|
|
||||||
where: eq(deployments.applicationId, applicationId),
|
|
||||||
with: {
|
|
||||||
application: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!application) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Deployment not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return application;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createDeployment = async (
|
|
||||||
deployment: Omit<
|
|
||||||
typeof apiCreateDeployment._type,
|
|
||||||
"deploymentId" | "createdAt" | "status" | "logPath"
|
|
||||||
>,
|
|
||||||
) => {
|
|
||||||
const application = await findApplicationById(deployment.applicationId);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// await removeLastTenDeployments(deployment.applicationId);
|
|
||||||
const { LOGS_PATH } = paths(!!application.serverId);
|
|
||||||
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
|
||||||
const fileName = `${application.appName}-${formattedDateTime}.log`;
|
|
||||||
const logFilePath = path.join(LOGS_PATH, application.appName, fileName);
|
|
||||||
|
|
||||||
if (application.serverId) {
|
|
||||||
const server = await findServerById(application.serverId);
|
|
||||||
|
|
||||||
const command = `
|
|
||||||
mkdir -p ${LOGS_PATH}/${application.appName};
|
|
||||||
echo "Initializing deployment" >> ${logFilePath};
|
|
||||||
`;
|
|
||||||
|
|
||||||
await execAsyncRemote(server.serverId, command);
|
|
||||||
} else {
|
|
||||||
await fsPromises.mkdir(path.join(LOGS_PATH, application.appName), {
|
|
||||||
recursive: true,
|
|
||||||
});
|
|
||||||
await fsPromises.writeFile(logFilePath, "Initializing deployment");
|
|
||||||
}
|
|
||||||
|
|
||||||
const deploymentCreate = await db
|
|
||||||
.insert(deployments)
|
|
||||||
.values({
|
|
||||||
applicationId: deployment.applicationId,
|
|
||||||
title: deployment.title || "Deployment",
|
|
||||||
status: "running",
|
|
||||||
logPath: logFilePath,
|
|
||||||
description: deployment.description || "",
|
|
||||||
})
|
|
||||||
.returning();
|
|
||||||
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error to create the deployment",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return deploymentCreate[0];
|
|
||||||
} catch (error) {
|
|
||||||
await updateApplicationStatus(application.applicationId, "error");
|
|
||||||
console.log(error);
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error to create the deployment",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createDeploymentCompose = async (
|
|
||||||
deployment: Omit<
|
|
||||||
typeof apiCreateDeploymentCompose._type,
|
|
||||||
"deploymentId" | "createdAt" | "status" | "logPath"
|
|
||||||
>,
|
|
||||||
) => {
|
|
||||||
const compose = await findComposeById(deployment.composeId);
|
|
||||||
try {
|
|
||||||
// await removeLastTenComposeDeployments(deployment.composeId);
|
|
||||||
const { LOGS_PATH } = paths(!!compose.serverId);
|
|
||||||
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);
|
|
||||||
|
|
||||||
if (compose.serverId) {
|
|
||||||
const server = await findServerById(compose.serverId);
|
|
||||||
|
|
||||||
const command = `
|
|
||||||
mkdir -p ${LOGS_PATH}/${compose.appName};
|
|
||||||
echo "Initializing deployment" >> ${logFilePath};
|
|
||||||
`;
|
|
||||||
|
|
||||||
await execAsyncRemote(server.serverId, command);
|
|
||||||
} else {
|
|
||||||
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",
|
|
||||||
description: deployment.description || "",
|
|
||||||
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) {
|
|
||||||
await updateCompose(compose.composeId, {
|
|
||||||
composeStatus: "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
|
|
||||||
.delete(deployments)
|
|
||||||
.where(eq(deployments.deploymentId, deploymentId))
|
|
||||||
.returning();
|
|
||||||
return deployment[0];
|
|
||||||
} catch (error) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error to delete this deployment",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const removeDeploymentsByApplicationId = async (
|
|
||||||
applicationId: string,
|
|
||||||
) => {
|
|
||||||
await db
|
|
||||||
.delete(deployments)
|
|
||||||
.where(eq(deployments.applicationId, applicationId))
|
|
||||||
.returning();
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeLastTenDeployments = async (applicationId: string) => {
|
|
||||||
const deploymentList = await db.query.deployments.findMany({
|
|
||||||
where: eq(deployments.applicationId, applicationId),
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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 { LOGS_PATH } = paths(!!application.serverId);
|
|
||||||
const logsPath = path.join(LOGS_PATH, appName);
|
|
||||||
if (application.serverId) {
|
|
||||||
await execAsyncRemote(application.serverId, `rm -rf ${logsPath}`);
|
|
||||||
} else {
|
|
||||||
await removeDirectoryIfExistsContent(logsPath);
|
|
||||||
}
|
|
||||||
await removeDeploymentsByApplicationId(applicationId);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const removeDeploymentsByComposeId = async (compose: Compose) => {
|
|
||||||
const { appName } = compose;
|
|
||||||
const { LOGS_PATH } = paths(!!compose.serverId);
|
|
||||||
const logsPath = path.join(LOGS_PATH, appName);
|
|
||||||
if (compose.serverId) {
|
|
||||||
await execAsyncRemote(compose.serverId, `rm -rf ${logsPath}`);
|
|
||||||
} else {
|
|
||||||
await removeDirectoryIfExistsContent(logsPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
await db
|
|
||||||
.delete(deployments)
|
|
||||||
.where(eq(deployments.composeId, compose.composeId))
|
|
||||||
.returning();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findAllDeploymentsByApplicationId = async (
|
|
||||||
applicationId: string,
|
|
||||||
) => {
|
|
||||||
const deploymentsList = await db.query.deployments.findMany({
|
|
||||||
where: eq(deployments.applicationId, applicationId),
|
|
||||||
orderBy: desc(deployments.createdAt),
|
|
||||||
});
|
|
||||||
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>,
|
|
||||||
) => {
|
|
||||||
const application = await db
|
|
||||||
.update(deployments)
|
|
||||||
.set({
|
|
||||||
...deploymentData,
|
|
||||||
})
|
|
||||||
.where(eq(deployments.deploymentId, deploymentId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
return application;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateDeploymentStatus = async (
|
|
||||||
deploymentId: string,
|
|
||||||
deploymentStatus: Deployment["status"],
|
|
||||||
) => {
|
|
||||||
const application = await db
|
|
||||||
.update(deployments)
|
|
||||||
.set({
|
|
||||||
status: deploymentStatus,
|
|
||||||
})
|
|
||||||
.where(eq(deployments.deploymentId, deploymentId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
return application;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createServerDeployment = async (
|
|
||||||
deployment: Omit<
|
|
||||||
typeof apiCreateDeploymentServer._type,
|
|
||||||
"deploymentId" | "createdAt" | "status" | "logPath"
|
|
||||||
>,
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
const { LOGS_PATH } = paths();
|
|
||||||
|
|
||||||
const server = await findServerById(deployment.serverId);
|
|
||||||
await removeLastFiveDeployments(deployment.serverId);
|
|
||||||
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
|
||||||
const fileName = `${server.appName}-${formattedDateTime}.log`;
|
|
||||||
const logFilePath = path.join(LOGS_PATH, server.appName, fileName);
|
|
||||||
await fsPromises.mkdir(path.join(LOGS_PATH, server.appName), {
|
|
||||||
recursive: true,
|
|
||||||
});
|
|
||||||
await fsPromises.writeFile(logFilePath, "Initializing Setup Server");
|
|
||||||
const deploymentCreate = await db
|
|
||||||
.insert(deployments)
|
|
||||||
.values({
|
|
||||||
serverId: server.serverId,
|
|
||||||
title: deployment.title || "Deployment",
|
|
||||||
description: deployment.description || "",
|
|
||||||
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 removeLastFiveDeployments = async (serverId: string) => {
|
|
||||||
const deploymentList = await db.query.deployments.findMany({
|
|
||||||
where: eq(deployments.serverId, serverId),
|
|
||||||
orderBy: desc(deployments.createdAt),
|
|
||||||
});
|
|
||||||
if (deploymentList.length >= 5) {
|
|
||||||
const deploymentsToDelete = deploymentList.slice(4);
|
|
||||||
for (const oldDeployment of deploymentsToDelete) {
|
|
||||||
const logPath = path.join(oldDeployment.logPath);
|
|
||||||
if (existsSync(logPath)) {
|
|
||||||
await fsPromises.unlink(logPath);
|
|
||||||
}
|
|
||||||
await removeDeployment(oldDeployment.deploymentId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const removeDeploymentsByServerId = async (server: Server) => {
|
|
||||||
const { LOGS_PATH } = paths();
|
|
||||||
const { appName } = server;
|
|
||||||
const logsPath = path.join(LOGS_PATH, appName);
|
|
||||||
await removeDirectoryIfExistsContent(logsPath);
|
|
||||||
await db
|
|
||||||
.delete(deployments)
|
|
||||||
.where(eq(deployments.serverId, server.serverId))
|
|
||||||
.returning();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findAllDeploymentsByServerId = async (serverId: string) => {
|
|
||||||
const deploymentsList = await db.query.deployments.findMany({
|
|
||||||
where: eq(deployments.serverId, serverId),
|
|
||||||
orderBy: desc(deployments.createdAt),
|
|
||||||
});
|
|
||||||
return deploymentsList;
|
|
||||||
};
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import { db } from "@/server/db";
|
|
||||||
import { type apiCreateDestination, destinations } from "@/server/db/schema";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import { findAdmin } from "./admin";
|
|
||||||
|
|
||||||
export type Destination = typeof destinations.$inferSelect;
|
|
||||||
|
|
||||||
export const createDestintation = async (
|
|
||||||
input: typeof apiCreateDestination._type,
|
|
||||||
) => {
|
|
||||||
const adminResponse = await findAdmin();
|
|
||||||
const newDestination = await db
|
|
||||||
.insert(destinations)
|
|
||||||
.values({
|
|
||||||
...input,
|
|
||||||
adminId: adminResponse.adminId,
|
|
||||||
})
|
|
||||||
.returning()
|
|
||||||
.then((value) => value[0]);
|
|
||||||
|
|
||||||
if (!newDestination) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error input: Inserting destination",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return newDestination;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findDestinationById = async (destinationId: string) => {
|
|
||||||
const destination = await db.query.destinations.findFirst({
|
|
||||||
where: eq(destinations.destinationId, destinationId),
|
|
||||||
});
|
|
||||||
if (!destination) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Destination not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return destination;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const removeDestinationById = async (destinationId: string) => {
|
|
||||||
const result = await db
|
|
||||||
.delete(destinations)
|
|
||||||
.where(eq(destinations.destinationId, destinationId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
return result[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateDestinationById = async (
|
|
||||||
destinationId: string,
|
|
||||||
destinationData: Partial<Destination>,
|
|
||||||
) => {
|
|
||||||
const result = await db
|
|
||||||
.update(destinations)
|
|
||||||
.set({
|
|
||||||
...destinationData,
|
|
||||||
})
|
|
||||||
.where(eq(destinations.destinationId, destinationId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
return result[0];
|
|
||||||
};
|
|
||||||
@@ -1,223 +0,0 @@
|
|||||||
import { execAsync, execAsyncRemote } from "@/server/utils/process/execAsync";
|
|
||||||
|
|
||||||
export const getContainers = async (serverId?: string | null) => {
|
|
||||||
try {
|
|
||||||
const command =
|
|
||||||
"docker ps -a --format 'CONTAINER ID : {{.ID}} | Name: {{.Names}} | Image: {{.Image}} | Ports: {{.Ports}} | State: {{.State}} | Status: {{.Status}}'";
|
|
||||||
let stdout = "";
|
|
||||||
let stderr = "";
|
|
||||||
|
|
||||||
if (serverId) {
|
|
||||||
const result = await execAsyncRemote(serverId, command);
|
|
||||||
|
|
||||||
stdout = result.stdout;
|
|
||||||
stderr = result.stderr;
|
|
||||||
} else {
|
|
||||||
const result = await execAsync(command);
|
|
||||||
stdout = result.stdout;
|
|
||||||
stderr = result.stderr;
|
|
||||||
}
|
|
||||||
if (stderr) {
|
|
||||||
console.error(`Error: ${stderr}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lines = stdout.trim().split("\n");
|
|
||||||
|
|
||||||
const containers = lines
|
|
||||||
.map((line) => {
|
|
||||||
const parts = line.split(" | ");
|
|
||||||
const containerId = parts[0]
|
|
||||||
? parts[0].replace("CONTAINER ID : ", "").trim()
|
|
||||||
: "No container id";
|
|
||||||
const name = parts[1]
|
|
||||||
? parts[1].replace("Name: ", "").trim()
|
|
||||||
: "No container name";
|
|
||||||
const image = parts[2]
|
|
||||||
? parts[2].replace("Image: ", "").trim()
|
|
||||||
: "No image";
|
|
||||||
const ports = parts[3]
|
|
||||||
? parts[3].replace("Ports: ", "").trim()
|
|
||||||
: "No ports";
|
|
||||||
const state = parts[4]
|
|
||||||
? parts[4].replace("State: ", "").trim()
|
|
||||||
: "No state";
|
|
||||||
const status = parts[5]
|
|
||||||
? parts[5].replace("Status: ", "").trim()
|
|
||||||
: "No status";
|
|
||||||
return {
|
|
||||||
containerId,
|
|
||||||
name,
|
|
||||||
image,
|
|
||||||
ports,
|
|
||||||
state,
|
|
||||||
status,
|
|
||||||
serverId,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter((container) => !container.name.includes("dokploy"));
|
|
||||||
|
|
||||||
return containers;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getConfig = async (
|
|
||||||
containerId: string,
|
|
||||||
serverId?: string | null,
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
const command = `docker inspect ${containerId} --format='{{json .}}'`;
|
|
||||||
let stdout = "";
|
|
||||||
let stderr = "";
|
|
||||||
if (serverId) {
|
|
||||||
const result = await execAsyncRemote(serverId, command);
|
|
||||||
stdout = result.stdout;
|
|
||||||
stderr = result.stderr;
|
|
||||||
} else {
|
|
||||||
const result = await execAsync(command);
|
|
||||||
stdout = result.stdout;
|
|
||||||
stderr = result.stderr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stderr) {
|
|
||||||
console.error(`Error: ${stderr}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = JSON.parse(stdout);
|
|
||||||
|
|
||||||
return config;
|
|
||||||
} catch (error) {}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getContainersByAppNameMatch = async (
|
|
||||||
appName: string,
|
|
||||||
appType?: "stack" | "docker-compose",
|
|
||||||
serverId?: string,
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
let result: string[] = [];
|
|
||||||
const cmd =
|
|
||||||
"docker ps -a --format 'CONTAINER ID : {{.ID}} | Name: {{.Names}} | State: {{.State}}'";
|
|
||||||
|
|
||||||
const command =
|
|
||||||
appType === "docker-compose"
|
|
||||||
? `${cmd} --filter='label=com.docker.compose.project=${appName}'`
|
|
||||||
: `${cmd} | grep ${appName}`;
|
|
||||||
if (serverId) {
|
|
||||||
const { stdout, stderr } = await execAsyncRemote(serverId, command);
|
|
||||||
|
|
||||||
if (stderr) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!stdout) return [];
|
|
||||||
result = stdout.trim().split("\n");
|
|
||||||
} else {
|
|
||||||
const { stdout, stderr } = await execAsync(command);
|
|
||||||
|
|
||||||
if (stderr) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!stdout) return [];
|
|
||||||
|
|
||||||
result = stdout.trim().split("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
const containers = result.map((line) => {
|
|
||||||
const parts = line.split(" | ");
|
|
||||||
const containerId = parts[0]
|
|
||||||
? parts[0].replace("CONTAINER ID : ", "").trim()
|
|
||||||
: "No container id";
|
|
||||||
const name = parts[1]
|
|
||||||
? parts[1].replace("Name: ", "").trim()
|
|
||||||
: "No container name";
|
|
||||||
|
|
||||||
const state = parts[2]
|
|
||||||
? parts[2].replace("State: ", "").trim()
|
|
||||||
: "No state";
|
|
||||||
return {
|
|
||||||
containerId,
|
|
||||||
name,
|
|
||||||
state,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return containers || [];
|
|
||||||
} catch (error) {}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getContainersByAppLabel = async (
|
|
||||||
appName: string,
|
|
||||||
serverId?: string,
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
let stdout = "";
|
|
||||||
let stderr = "";
|
|
||||||
|
|
||||||
const command = `docker ps --filter "label=com.docker.swarm.service.name=${appName}" --format 'CONTAINER ID : {{.ID}} | Name: {{.Names}} | State: {{.State}}'`;
|
|
||||||
if (serverId) {
|
|
||||||
const result = await execAsyncRemote(serverId, command);
|
|
||||||
stdout = result.stdout;
|
|
||||||
stderr = result.stderr;
|
|
||||||
} else {
|
|
||||||
const result = await execAsync(command);
|
|
||||||
stdout = result.stdout;
|
|
||||||
stderr = result.stderr;
|
|
||||||
}
|
|
||||||
if (stderr) {
|
|
||||||
console.error(`Error: ${stderr}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!stdout) return [];
|
|
||||||
|
|
||||||
const lines = stdout.trim().split("\n");
|
|
||||||
|
|
||||||
const containers = lines.map((line) => {
|
|
||||||
const parts = line.split(" | ");
|
|
||||||
const containerId = parts[0]
|
|
||||||
? parts[0].replace("CONTAINER ID : ", "").trim()
|
|
||||||
: "No container id";
|
|
||||||
const name = parts[1]
|
|
||||||
? parts[1].replace("Name: ", "").trim()
|
|
||||||
: "No container name";
|
|
||||||
const state = parts[2]
|
|
||||||
? parts[2].replace("State: ", "").trim()
|
|
||||||
: "No state";
|
|
||||||
return {
|
|
||||||
containerId,
|
|
||||||
name,
|
|
||||||
state,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return containers || [];
|
|
||||||
} catch (error) {}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const containerRestart = async (containerId: string) => {
|
|
||||||
try {
|
|
||||||
const { stdout, stderr } = await execAsync(
|
|
||||||
`docker container restart ${containerId}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (stderr) {
|
|
||||||
console.error(`Error: ${stderr}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = JSON.parse(stdout);
|
|
||||||
|
|
||||||
return config;
|
|
||||||
} catch (error) {}
|
|
||||||
};
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
import { db } from "@/server/db";
|
|
||||||
import { type apiCreateDomain, domains } from "@/server/db/schema";
|
|
||||||
import { manageDomain } from "@/server/utils/traefik/domain";
|
|
||||||
import { generateRandomDomain } from "@/templates/utils";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import { findAdmin, findAdminById } from "./admin";
|
|
||||||
import { findApplicationById } from "./application";
|
|
||||||
import { findServerById } from "./server";
|
|
||||||
|
|
||||||
export type Domain = typeof domains.$inferSelect;
|
|
||||||
|
|
||||||
export const createDomain = async (input: typeof apiCreateDomain._type) => {
|
|
||||||
const result = await db.transaction(async (tx) => {
|
|
||||||
const domain = await tx
|
|
||||||
.insert(domains)
|
|
||||||
.values({
|
|
||||||
...input,
|
|
||||||
})
|
|
||||||
.returning()
|
|
||||||
.then((response) => response[0]);
|
|
||||||
|
|
||||||
if (!domain) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error creating domain",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (domain.applicationId) {
|
|
||||||
const application = await findApplicationById(domain.applicationId);
|
|
||||||
await manageDomain(application, domain);
|
|
||||||
}
|
|
||||||
|
|
||||||
return domain;
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const generateTraefikMeDomain = async (
|
|
||||||
appName: string,
|
|
||||||
adminId: string,
|
|
||||||
serverId?: string,
|
|
||||||
) => {
|
|
||||||
if (serverId) {
|
|
||||||
const server = await findServerById(serverId);
|
|
||||||
return generateRandomDomain({
|
|
||||||
serverIp: server.ipAddress,
|
|
||||||
projectName: appName,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV === "development") {
|
|
||||||
return generateRandomDomain({
|
|
||||||
serverIp: "",
|
|
||||||
projectName: appName,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const admin = await findAdminById(adminId);
|
|
||||||
return generateRandomDomain({
|
|
||||||
serverIp: admin?.serverIp || "",
|
|
||||||
projectName: appName,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const generateWildcardDomain = (
|
|
||||||
appName: string,
|
|
||||||
serverDomain: string,
|
|
||||||
) => {
|
|
||||||
return `${appName}-${serverDomain}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findDomainById = async (domainId: string) => {
|
|
||||||
const domain = await db.query.domains.findFirst({
|
|
||||||
where: eq(domains.domainId, domainId),
|
|
||||||
with: {
|
|
||||||
application: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!domain) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Domain not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return domain;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findDomainsByApplicationId = async (applicationId: string) => {
|
|
||||||
const domainsArray = await db.query.domains.findMany({
|
|
||||||
where: eq(domains.applicationId, applicationId),
|
|
||||||
with: {
|
|
||||||
application: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return domainsArray;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findDomainsByComposeId = async (composeId: string) => {
|
|
||||||
const domainsArray = await db.query.domains.findMany({
|
|
||||||
where: eq(domains.composeId, composeId),
|
|
||||||
with: {
|
|
||||||
compose: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return domainsArray;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateDomainById = async (
|
|
||||||
domainId: string,
|
|
||||||
domainData: Partial<Domain>,
|
|
||||||
) => {
|
|
||||||
const domain = await db
|
|
||||||
.update(domains)
|
|
||||||
.set({
|
|
||||||
...domainData,
|
|
||||||
})
|
|
||||||
.where(eq(domains.domainId, domainId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
return domain[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const removeDomainById = async (domainId: string) => {
|
|
||||||
await findDomainById(domainId);
|
|
||||||
// TODO: fix order
|
|
||||||
const result = await db
|
|
||||||
.delete(domains)
|
|
||||||
.where(eq(domains.domainId, domainId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
return result[0];
|
|
||||||
};
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { db } from "@/server/db";
|
|
||||||
import { type apiCreateGithub, gitProvider, github } from "@/server/db/schema";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
|
|
||||||
export type GitProvider = typeof gitProvider.$inferSelect;
|
|
||||||
|
|
||||||
export const removeGitProvider = async (gitProviderId: string) => {
|
|
||||||
const result = await db
|
|
||||||
.delete(gitProvider)
|
|
||||||
.where(eq(gitProvider.gitProviderId, gitProviderId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
return result[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateGitProvider = async (
|
|
||||||
gitProviderId: string,
|
|
||||||
input: Partial<GitProvider>,
|
|
||||||
) => {
|
|
||||||
return await db
|
|
||||||
.update(gitProvider)
|
|
||||||
.set({
|
|
||||||
...input,
|
|
||||||
})
|
|
||||||
.where(eq(gitProvider.gitProviderId, gitProviderId))
|
|
||||||
.returning()
|
|
||||||
.then((response) => response[0]);
|
|
||||||
};
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
import { db } from "@/server/db";
|
|
||||||
import { type apiCreateGithub, gitProvider, github } from "@/server/db/schema";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
|
|
||||||
export type Github = typeof github.$inferSelect;
|
|
||||||
export const createGithub = async (input: typeof apiCreateGithub._type) => {
|
|
||||||
return await db.transaction(async (tx) => {
|
|
||||||
const newGitProvider = await tx
|
|
||||||
.insert(gitProvider)
|
|
||||||
.values({
|
|
||||||
providerType: "github",
|
|
||||||
authId: input.authId,
|
|
||||||
name: input.name,
|
|
||||||
})
|
|
||||||
.returning()
|
|
||||||
.then((response) => response[0]);
|
|
||||||
|
|
||||||
if (!newGitProvider) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error to create the git provider",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return await tx
|
|
||||||
.insert(github)
|
|
||||||
.values({
|
|
||||||
...input,
|
|
||||||
gitProviderId: newGitProvider?.gitProviderId,
|
|
||||||
})
|
|
||||||
.returning()
|
|
||||||
.then((response) => response[0]);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findGithubById = async (githubId: string) => {
|
|
||||||
const githubProviderResult = await db.query.github.findFirst({
|
|
||||||
where: eq(github.githubId, githubId),
|
|
||||||
with: {
|
|
||||||
gitProvider: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!githubProviderResult) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Github Provider not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return githubProviderResult;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const haveGithubRequirements = (github: Github) => {
|
|
||||||
return !!(
|
|
||||||
github?.githubAppId &&
|
|
||||||
github?.githubPrivateKey &&
|
|
||||||
github?.githubInstallationId
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateGithub = async (
|
|
||||||
githubId: string,
|
|
||||||
input: Partial<Github>,
|
|
||||||
) => {
|
|
||||||
return await db
|
|
||||||
.update(github)
|
|
||||||
.set({
|
|
||||||
...input,
|
|
||||||
})
|
|
||||||
.where(eq(github.githubId, githubId))
|
|
||||||
.returning()
|
|
||||||
.then((response) => response[0]);
|
|
||||||
};
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
import { db } from "@/server/db";
|
|
||||||
import {
|
|
||||||
type apiCreateGitlab,
|
|
||||||
type bitbucket,
|
|
||||||
gitProvider,
|
|
||||||
type github,
|
|
||||||
gitlab,
|
|
||||||
} from "@/server/db/schema";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
|
|
||||||
export type Github = typeof github.$inferSelect;
|
|
||||||
export type Bitbucket = typeof bitbucket.$inferSelect;
|
|
||||||
export type Gitlab = typeof gitlab.$inferSelect;
|
|
||||||
|
|
||||||
export const createGitlab = async (input: typeof apiCreateGitlab._type) => {
|
|
||||||
return await db.transaction(async (tx) => {
|
|
||||||
const newGitProvider = await tx
|
|
||||||
.insert(gitProvider)
|
|
||||||
.values({
|
|
||||||
providerType: "gitlab",
|
|
||||||
authId: input.authId,
|
|
||||||
name: input.name,
|
|
||||||
})
|
|
||||||
.returning()
|
|
||||||
.then((response) => response[0]);
|
|
||||||
|
|
||||||
if (!newGitProvider) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error to create the git provider",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await tx
|
|
||||||
.insert(gitlab)
|
|
||||||
.values({
|
|
||||||
...input,
|
|
||||||
gitProviderId: newGitProvider?.gitProviderId,
|
|
||||||
})
|
|
||||||
.returning()
|
|
||||||
.then((response) => response[0]);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findGitlabById = async (gitlabId: string) => {
|
|
||||||
const gitlabProviderResult = await db.query.gitlab.findFirst({
|
|
||||||
where: eq(gitlab.gitlabId, gitlabId),
|
|
||||||
with: {
|
|
||||||
gitProvider: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!gitlabProviderResult) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Gitlab Provider not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return gitlabProviderResult;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateGitlab = async (
|
|
||||||
gitlabId: string,
|
|
||||||
input: Partial<Gitlab>,
|
|
||||||
) => {
|
|
||||||
return await db
|
|
||||||
.update(gitlab)
|
|
||||||
.set({
|
|
||||||
...input,
|
|
||||||
})
|
|
||||||
.where(eq(gitlab.gitlabId, gitlabId))
|
|
||||||
.returning()
|
|
||||||
.then((response) => response[0]);
|
|
||||||
};
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
import { generateRandomPassword } from "@/server/auth/random-password";
|
|
||||||
import { db } from "@/server/db";
|
|
||||||
import { type apiCreateMariaDB, backups, mariadb } from "@/server/db/schema";
|
|
||||||
import { generateAppName } from "@/server/db/schema";
|
|
||||||
import { buildMariadb } from "@/server/utils/databases/mariadb";
|
|
||||||
import { pullImage } from "@/server/utils/docker/utils";
|
|
||||||
import { generatePassword } from "@/templates/utils";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import { eq, getTableColumns } from "drizzle-orm";
|
|
||||||
import { validUniqueServerAppName } from "./project";
|
|
||||||
|
|
||||||
import { execAsyncRemote } from "@/server/utils/process/execAsync";
|
|
||||||
|
|
||||||
export type Mariadb = typeof mariadb.$inferSelect;
|
|
||||||
|
|
||||||
export const createMariadb = async (input: typeof apiCreateMariaDB._type) => {
|
|
||||||
input.appName =
|
|
||||||
`${input.appName}-${generatePassword(6)}` || generateAppName("mariadb");
|
|
||||||
if (input.appName) {
|
|
||||||
const valid = await validUniqueServerAppName(input.appName);
|
|
||||||
|
|
||||||
if (!valid) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "CONFLICT",
|
|
||||||
message: "Service with this 'AppName' already exists",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const newMariadb = await db
|
|
||||||
.insert(mariadb)
|
|
||||||
.values({
|
|
||||||
...input,
|
|
||||||
databasePassword: input.databasePassword
|
|
||||||
? input.databasePassword
|
|
||||||
: (await generateRandomPassword()).randomPassword,
|
|
||||||
databaseRootPassword: input.databaseRootPassword
|
|
||||||
? input.databaseRootPassword
|
|
||||||
: (await generateRandomPassword()).randomPassword,
|
|
||||||
})
|
|
||||||
.returning()
|
|
||||||
.then((value) => value[0]);
|
|
||||||
|
|
||||||
if (!newMariadb) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error input: Inserting mariadb database",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return newMariadb;
|
|
||||||
};
|
|
||||||
|
|
||||||
// https://github.com/drizzle-team/drizzle-orm/discussions/1483#discussioncomment-7523881
|
|
||||||
export const findMariadbById = async (mariadbId: string) => {
|
|
||||||
const result = await db.query.mariadb.findFirst({
|
|
||||||
where: eq(mariadb.mariadbId, mariadbId),
|
|
||||||
with: {
|
|
||||||
project: true,
|
|
||||||
mounts: true,
|
|
||||||
server: true,
|
|
||||||
backups: {
|
|
||||||
with: {
|
|
||||||
destination: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!result) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Mariadb not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateMariadbById = async (
|
|
||||||
mariadbId: string,
|
|
||||||
mariadbData: Partial<Mariadb>,
|
|
||||||
) => {
|
|
||||||
const result = await db
|
|
||||||
.update(mariadb)
|
|
||||||
.set({
|
|
||||||
...mariadbData,
|
|
||||||
})
|
|
||||||
.where(eq(mariadb.mariadbId, mariadbId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
return result[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const removeMariadbById = async (mariadbId: string) => {
|
|
||||||
const result = await db
|
|
||||||
.delete(mariadb)
|
|
||||||
.where(eq(mariadb.mariadbId, mariadbId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
return result[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findMariadbByBackupId = async (backupId: string) => {
|
|
||||||
const result = await db
|
|
||||||
.select({
|
|
||||||
...getTableColumns(mariadb),
|
|
||||||
})
|
|
||||||
.from(mariadb)
|
|
||||||
.innerJoin(backups, eq(mariadb.mariadbId, backups.mariadbId))
|
|
||||||
.where(eq(backups.backupId, backupId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!result || !result[0]) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "MariaDB not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return result[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deployMariadb = async (mariadbId: string) => {
|
|
||||||
const mariadb = await findMariadbById(mariadbId);
|
|
||||||
try {
|
|
||||||
if (mariadb.serverId) {
|
|
||||||
await execAsyncRemote(
|
|
||||||
mariadb.serverId,
|
|
||||||
`docker pull ${mariadb.dockerImage}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await pullImage(mariadb.dockerImage);
|
|
||||||
}
|
|
||||||
|
|
||||||
await buildMariadb(mariadb);
|
|
||||||
await updateMariadbById(mariadbId, {
|
|
||||||
applicationStatus: "done",
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
await updateMariadbById(mariadbId, {
|
|
||||||
applicationStatus: "error",
|
|
||||||
});
|
|
||||||
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
|
||||||
message: `Error on deploy mariadb${error}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return mariadb;
|
|
||||||
};
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
import { generateRandomPassword } from "@/server/auth/random-password";
|
|
||||||
import { db } from "@/server/db";
|
|
||||||
import { type apiCreateMongo, backups, mongo } from "@/server/db/schema";
|
|
||||||
import { generateAppName } from "@/server/db/schema";
|
|
||||||
import { buildMongo } from "@/server/utils/databases/mongo";
|
|
||||||
import { pullImage } from "@/server/utils/docker/utils";
|
|
||||||
import { generatePassword } from "@/templates/utils";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import { eq, getTableColumns } from "drizzle-orm";
|
|
||||||
import { validUniqueServerAppName } from "./project";
|
|
||||||
|
|
||||||
import { execAsyncRemote } from "@/server/utils/process/execAsync";
|
|
||||||
|
|
||||||
export type Mongo = typeof mongo.$inferSelect;
|
|
||||||
|
|
||||||
export const createMongo = async (input: typeof apiCreateMongo._type) => {
|
|
||||||
input.appName =
|
|
||||||
`${input.appName}-${generatePassword(6)}` || generateAppName("postgres");
|
|
||||||
if (input.appName) {
|
|
||||||
const valid = await validUniqueServerAppName(input.appName);
|
|
||||||
|
|
||||||
if (!valid) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "CONFLICT",
|
|
||||||
message: "Service with this 'AppName' already exists",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const newMongo = await db
|
|
||||||
.insert(mongo)
|
|
||||||
.values({
|
|
||||||
...input,
|
|
||||||
databasePassword: input.databasePassword
|
|
||||||
? input.databasePassword
|
|
||||||
: (await generateRandomPassword()).randomPassword,
|
|
||||||
})
|
|
||||||
.returning()
|
|
||||||
.then((value) => value[0]);
|
|
||||||
|
|
||||||
if (!newMongo) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error input: Inserting mongo database",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return newMongo;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findMongoById = async (mongoId: string) => {
|
|
||||||
const result = await db.query.mongo.findFirst({
|
|
||||||
where: eq(mongo.mongoId, mongoId),
|
|
||||||
with: {
|
|
||||||
project: true,
|
|
||||||
mounts: true,
|
|
||||||
server: true,
|
|
||||||
backups: {
|
|
||||||
with: {
|
|
||||||
destination: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!result) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Mongo not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateMongoById = async (
|
|
||||||
mongoId: string,
|
|
||||||
postgresData: Partial<Mongo>,
|
|
||||||
) => {
|
|
||||||
const result = await db
|
|
||||||
.update(mongo)
|
|
||||||
.set({
|
|
||||||
...postgresData,
|
|
||||||
})
|
|
||||||
.where(eq(mongo.mongoId, mongoId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
return result[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findMongoByBackupId = async (backupId: string) => {
|
|
||||||
const result = await db
|
|
||||||
.select({
|
|
||||||
...getTableColumns(mongo),
|
|
||||||
})
|
|
||||||
.from(mongo)
|
|
||||||
.innerJoin(backups, eq(mongo.mongoId, backups.mongoId))
|
|
||||||
.where(eq(backups.backupId, backupId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!result || !result[0]) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Mongo not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return result[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const removeMongoById = async (mongoId: string) => {
|
|
||||||
const result = await db
|
|
||||||
.delete(mongo)
|
|
||||||
.where(eq(mongo.mongoId, mongoId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
return result[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deployMongo = async (mongoId: string) => {
|
|
||||||
const mongo = await findMongoById(mongoId);
|
|
||||||
try {
|
|
||||||
if (mongo.serverId) {
|
|
||||||
await execAsyncRemote(mongo.serverId, `docker pull ${mongo.dockerImage}`);
|
|
||||||
} else {
|
|
||||||
await pullImage(mongo.dockerImage);
|
|
||||||
}
|
|
||||||
|
|
||||||
await buildMongo(mongo);
|
|
||||||
await updateMongoById(mongoId, {
|
|
||||||
applicationStatus: "done",
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
await updateMongoById(mongoId, {
|
|
||||||
applicationStatus: "error",
|
|
||||||
});
|
|
||||||
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
|
||||||
message: `Error on deploy mongo${error}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return mongo;
|
|
||||||
};
|
|
||||||
@@ -1,280 +0,0 @@
|
|||||||
import path from "node:path";
|
|
||||||
import { paths } from "@/server/constants";
|
|
||||||
import { db } from "@/server/db";
|
|
||||||
import {
|
|
||||||
type ServiceType,
|
|
||||||
type apiCreateMount,
|
|
||||||
mounts,
|
|
||||||
} from "@/server/db/schema";
|
|
||||||
import { createFile, getCreateFileCommand } from "@/server/utils/docker/utils";
|
|
||||||
import { removeFileOrDirectory } from "@/server/utils/filesystem/directory";
|
|
||||||
import { execAsyncRemote } from "@/server/utils/process/execAsync";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import { type SQL, eq, sql } from "drizzle-orm";
|
|
||||||
|
|
||||||
export type Mount = typeof mounts.$inferSelect;
|
|
||||||
|
|
||||||
export const createMount = async (input: typeof apiCreateMount._type) => {
|
|
||||||
try {
|
|
||||||
const { serviceId, ...rest } = input;
|
|
||||||
const value = await db
|
|
||||||
.insert(mounts)
|
|
||||||
.values({
|
|
||||||
...rest,
|
|
||||||
...(input.serviceType === "application" && {
|
|
||||||
applicationId: serviceId,
|
|
||||||
}),
|
|
||||||
...(input.serviceType === "postgres" && {
|
|
||||||
postgresId: serviceId,
|
|
||||||
}),
|
|
||||||
...(input.serviceType === "mariadb" && {
|
|
||||||
mariadbId: serviceId,
|
|
||||||
}),
|
|
||||||
...(input.serviceType === "mongo" && {
|
|
||||||
mongoId: serviceId,
|
|
||||||
}),
|
|
||||||
...(input.serviceType === "mysql" && {
|
|
||||||
mysqlId: serviceId,
|
|
||||||
}),
|
|
||||||
...(input.serviceType === "redis" && {
|
|
||||||
redisId: serviceId,
|
|
||||||
}),
|
|
||||||
...(input.serviceType === "compose" && {
|
|
||||||
composeId: serviceId,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.returning()
|
|
||||||
.then((value) => value[0]);
|
|
||||||
|
|
||||||
if (!value) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error input: Inserting mount",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value.type === "file") {
|
|
||||||
await createFileMount(value.mountId);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error to create the mount",
|
|
||||||
cause: error,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createFileMount = async (mountId: string) => {
|
|
||||||
try {
|
|
||||||
const mount = await findMountById(mountId);
|
|
||||||
const baseFilePath = await getBaseFilesPath(mountId);
|
|
||||||
|
|
||||||
const serverId = await getServerId(mount);
|
|
||||||
|
|
||||||
if (serverId) {
|
|
||||||
const command = getCreateFileCommand(
|
|
||||||
baseFilePath,
|
|
||||||
mount.filePath || "",
|
|
||||||
mount.content || "",
|
|
||||||
);
|
|
||||||
await execAsyncRemote(serverId, command);
|
|
||||||
} else {
|
|
||||||
await createFile(baseFilePath, mount.filePath || "", mount.content || "");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`Error to create the file mount: ${error}`);
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error to create the mount",
|
|
||||||
cause: error,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findMountById = async (mountId: string) => {
|
|
||||||
const mount = await db.query.mounts.findFirst({
|
|
||||||
where: eq(mounts.mountId, mountId),
|
|
||||||
with: {
|
|
||||||
application: true,
|
|
||||||
postgres: true,
|
|
||||||
mariadb: true,
|
|
||||||
mongo: true,
|
|
||||||
mysql: true,
|
|
||||||
redis: true,
|
|
||||||
compose: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!mount) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Mount not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return mount;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateMount = async (
|
|
||||||
mountId: string,
|
|
||||||
mountData: Partial<Mount>,
|
|
||||||
) => {
|
|
||||||
return await db.transaction(async (transaction) => {
|
|
||||||
const mount = await db
|
|
||||||
.update(mounts)
|
|
||||||
.set({
|
|
||||||
...mountData,
|
|
||||||
})
|
|
||||||
.where(eq(mounts.mountId, mountId))
|
|
||||||
.returning()
|
|
||||||
.then((value) => value[0]);
|
|
||||||
|
|
||||||
if (!mount) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Mount not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mount.type === "file") {
|
|
||||||
await deleteFileMount(mountId);
|
|
||||||
await createFileMount(mountId);
|
|
||||||
}
|
|
||||||
return mount;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findMountsByApplicationId = async (
|
|
||||||
serviceId: string,
|
|
||||||
serviceType: ServiceType,
|
|
||||||
) => {
|
|
||||||
const sqlChunks: SQL[] = [];
|
|
||||||
|
|
||||||
switch (serviceType) {
|
|
||||||
case "application":
|
|
||||||
sqlChunks.push(eq(mounts.applicationId, serviceId));
|
|
||||||
break;
|
|
||||||
case "postgres":
|
|
||||||
sqlChunks.push(eq(mounts.postgresId, serviceId));
|
|
||||||
break;
|
|
||||||
case "mariadb":
|
|
||||||
sqlChunks.push(eq(mounts.mariadbId, serviceId));
|
|
||||||
break;
|
|
||||||
case "mongo":
|
|
||||||
sqlChunks.push(eq(mounts.mongoId, serviceId));
|
|
||||||
break;
|
|
||||||
case "mysql":
|
|
||||||
sqlChunks.push(eq(mounts.mysqlId, serviceId));
|
|
||||||
break;
|
|
||||||
case "redis":
|
|
||||||
sqlChunks.push(eq(mounts.redisId, serviceId));
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Error(`Unknown service type: ${serviceType}`);
|
|
||||||
}
|
|
||||||
const mount = await db.query.mounts.findMany({
|
|
||||||
where: sql.join(sqlChunks, sql.raw(" ")),
|
|
||||||
});
|
|
||||||
|
|
||||||
return mount;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteMount = async (mountId: string) => {
|
|
||||||
const { type } = await findMountById(mountId);
|
|
||||||
|
|
||||||
if (type === "file") {
|
|
||||||
await deleteFileMount(mountId);
|
|
||||||
}
|
|
||||||
|
|
||||||
const deletedMount = await db
|
|
||||||
.delete(mounts)
|
|
||||||
.where(eq(mounts.mountId, mountId))
|
|
||||||
.returning();
|
|
||||||
return deletedMount[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteFileMount = async (mountId: string) => {
|
|
||||||
const mount = await findMountById(mountId);
|
|
||||||
if (!mount.filePath) return;
|
|
||||||
const basePath = await getBaseFilesPath(mountId);
|
|
||||||
|
|
||||||
const fullPath = path.join(basePath, mount.filePath);
|
|
||||||
try {
|
|
||||||
const serverId = await getServerId(mount);
|
|
||||||
if (serverId) {
|
|
||||||
const command = `rm -rf ${fullPath}`;
|
|
||||||
await execAsyncRemote(serverId, command);
|
|
||||||
} else {
|
|
||||||
await removeFileOrDirectory(fullPath);
|
|
||||||
}
|
|
||||||
} catch (error) {}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getBaseFilesPath = async (mountId: string) => {
|
|
||||||
const mount = await findMountById(mountId);
|
|
||||||
|
|
||||||
let absoluteBasePath = "";
|
|
||||||
let appName = "";
|
|
||||||
let directoryPath = "";
|
|
||||||
|
|
||||||
if (mount.serviceType === "application" && mount.application) {
|
|
||||||
const { APPLICATIONS_PATH } = paths(!!mount.application.serverId);
|
|
||||||
absoluteBasePath = path.resolve(APPLICATIONS_PATH);
|
|
||||||
appName = mount.application.appName;
|
|
||||||
} else if (mount.serviceType === "postgres" && mount.postgres) {
|
|
||||||
const { APPLICATIONS_PATH } = paths(!!mount.postgres.serverId);
|
|
||||||
absoluteBasePath = path.resolve(APPLICATIONS_PATH);
|
|
||||||
appName = mount.postgres.appName;
|
|
||||||
} else if (mount.serviceType === "mariadb" && mount.mariadb) {
|
|
||||||
const { APPLICATIONS_PATH } = paths(!!mount.mariadb.serverId);
|
|
||||||
absoluteBasePath = path.resolve(APPLICATIONS_PATH);
|
|
||||||
appName = mount.mariadb.appName;
|
|
||||||
} else if (mount.serviceType === "mongo" && mount.mongo) {
|
|
||||||
const { APPLICATIONS_PATH } = paths(!!mount.mongo.serverId);
|
|
||||||
absoluteBasePath = path.resolve(APPLICATIONS_PATH);
|
|
||||||
appName = mount.mongo.appName;
|
|
||||||
} else if (mount.serviceType === "mysql" && mount.mysql) {
|
|
||||||
const { APPLICATIONS_PATH } = paths(!!mount.mysql.serverId);
|
|
||||||
absoluteBasePath = path.resolve(APPLICATIONS_PATH);
|
|
||||||
appName = mount.mysql.appName;
|
|
||||||
} else if (mount.serviceType === "redis" && mount.redis) {
|
|
||||||
const { APPLICATIONS_PATH } = paths(!!mount.redis.serverId);
|
|
||||||
absoluteBasePath = path.resolve(APPLICATIONS_PATH);
|
|
||||||
appName = mount.redis.appName;
|
|
||||||
} else if (mount.serviceType === "compose" && mount.compose) {
|
|
||||||
const { COMPOSE_PATH } = paths(!!mount.compose.serverId);
|
|
||||||
appName = mount.compose.appName;
|
|
||||||
absoluteBasePath = path.resolve(COMPOSE_PATH);
|
|
||||||
}
|
|
||||||
directoryPath = path.join(absoluteBasePath, appName, "files");
|
|
||||||
|
|
||||||
return directoryPath;
|
|
||||||
};
|
|
||||||
|
|
||||||
type MountNested = Awaited<ReturnType<typeof findMountById>>;
|
|
||||||
export const getServerId = async (mount: MountNested) => {
|
|
||||||
if (mount.serviceType === "application" && mount?.application?.serverId) {
|
|
||||||
return mount.application.serverId;
|
|
||||||
}
|
|
||||||
if (mount.serviceType === "postgres" && mount?.postgres?.serverId) {
|
|
||||||
return mount.postgres.serverId;
|
|
||||||
}
|
|
||||||
if (mount.serviceType === "mariadb" && mount?.mariadb?.serverId) {
|
|
||||||
return mount.mariadb.serverId;
|
|
||||||
}
|
|
||||||
if (mount.serviceType === "mongo" && mount?.mongo?.serverId) {
|
|
||||||
return mount.mongo.serverId;
|
|
||||||
}
|
|
||||||
if (mount.serviceType === "mysql" && mount?.mysql?.serverId) {
|
|
||||||
return mount.mysql.serverId;
|
|
||||||
}
|
|
||||||
if (mount.serviceType === "redis" && mount?.redis?.serverId) {
|
|
||||||
return mount.redis.serverId;
|
|
||||||
}
|
|
||||||
if (mount.serviceType === "compose" && mount?.compose?.serverId) {
|
|
||||||
return mount.compose.serverId;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
import { generateRandomPassword } from "@/server/auth/random-password";
|
|
||||||
import { db } from "@/server/db";
|
|
||||||
import { type apiCreateMySql, backups, mysql } from "@/server/db/schema";
|
|
||||||
import { generateAppName } from "@/server/db/schema";
|
|
||||||
import { buildMysql } from "@/server/utils/databases/mysql";
|
|
||||||
import { pullImage } from "@/server/utils/docker/utils";
|
|
||||||
import { generatePassword } from "@/templates/utils";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import { eq, getTableColumns } from "drizzle-orm";
|
|
||||||
import { validUniqueServerAppName } from "./project";
|
|
||||||
|
|
||||||
import { execAsyncRemote } from "@/server/utils/process/execAsync";
|
|
||||||
|
|
||||||
export type MySql = typeof mysql.$inferSelect;
|
|
||||||
|
|
||||||
export const createMysql = async (input: typeof apiCreateMySql._type) => {
|
|
||||||
input.appName =
|
|
||||||
`${input.appName}-${generatePassword(6)}` || generateAppName("mysql");
|
|
||||||
|
|
||||||
if (input.appName) {
|
|
||||||
const valid = await validUniqueServerAppName(input.appName);
|
|
||||||
|
|
||||||
if (!valid) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "CONFLICT",
|
|
||||||
message: "Service with this 'AppName' already exists",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const newMysql = await db
|
|
||||||
.insert(mysql)
|
|
||||||
.values({
|
|
||||||
...input,
|
|
||||||
databasePassword: input.databasePassword
|
|
||||||
? input.databasePassword
|
|
||||||
: (await generateRandomPassword()).randomPassword,
|
|
||||||
databaseRootPassword: input.databaseRootPassword
|
|
||||||
? input.databaseRootPassword
|
|
||||||
: (await generateRandomPassword()).randomPassword,
|
|
||||||
})
|
|
||||||
.returning()
|
|
||||||
.then((value) => value[0]);
|
|
||||||
|
|
||||||
if (!newMysql) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error input: Inserting mysql database",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return newMysql;
|
|
||||||
};
|
|
||||||
|
|
||||||
// https://github.com/drizzle-team/drizzle-orm/discussions/1483#discussioncomment-7523881
|
|
||||||
export const findMySqlById = async (mysqlId: string) => {
|
|
||||||
const result = await db.query.mysql.findFirst({
|
|
||||||
where: eq(mysql.mysqlId, mysqlId),
|
|
||||||
with: {
|
|
||||||
project: true,
|
|
||||||
mounts: true,
|
|
||||||
server: true,
|
|
||||||
backups: {
|
|
||||||
with: {
|
|
||||||
destination: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!result) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "MySql not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateMySqlById = async (
|
|
||||||
mysqlId: string,
|
|
||||||
mysqlData: Partial<MySql>,
|
|
||||||
) => {
|
|
||||||
const result = await db
|
|
||||||
.update(mysql)
|
|
||||||
.set({
|
|
||||||
...mysqlData,
|
|
||||||
})
|
|
||||||
.where(eq(mysql.mysqlId, mysqlId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
return result[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findMySqlByBackupId = async (backupId: string) => {
|
|
||||||
const result = await db
|
|
||||||
.select({
|
|
||||||
...getTableColumns(mysql),
|
|
||||||
})
|
|
||||||
.from(mysql)
|
|
||||||
.innerJoin(backups, eq(mysql.mysqlId, backups.mysqlId))
|
|
||||||
.where(eq(backups.backupId, backupId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!result || !result[0]) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Mysql not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return result[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const removeMySqlById = async (mysqlId: string) => {
|
|
||||||
const result = await db
|
|
||||||
.delete(mysql)
|
|
||||||
.where(eq(mysql.mysqlId, mysqlId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
return result[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deployMySql = async (mysqlId: string) => {
|
|
||||||
const mysql = await findMySqlById(mysqlId);
|
|
||||||
try {
|
|
||||||
if (mysql.serverId) {
|
|
||||||
await execAsyncRemote(mysql.serverId, `docker pull ${mysql.dockerImage}`);
|
|
||||||
} else {
|
|
||||||
await pullImage(mysql.dockerImage);
|
|
||||||
}
|
|
||||||
|
|
||||||
await buildMysql(mysql);
|
|
||||||
await updateMySqlById(mysqlId, {
|
|
||||||
applicationStatus: "done",
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
await updateMySqlById(mysqlId, {
|
|
||||||
applicationStatus: "error",
|
|
||||||
});
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
|
||||||
message: `Error on deploy mysql${error}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return mysql;
|
|
||||||
};
|
|
||||||
@@ -1,409 +0,0 @@
|
|||||||
import { db } from "@/server/db";
|
|
||||||
import {
|
|
||||||
type apiCreateDiscord,
|
|
||||||
type apiCreateEmail,
|
|
||||||
type apiCreateSlack,
|
|
||||||
type apiCreateTelegram,
|
|
||||||
type apiUpdateDiscord,
|
|
||||||
type apiUpdateEmail,
|
|
||||||
type apiUpdateSlack,
|
|
||||||
type apiUpdateTelegram,
|
|
||||||
discord,
|
|
||||||
email,
|
|
||||||
notifications,
|
|
||||||
slack,
|
|
||||||
telegram,
|
|
||||||
} from "@/server/db/schema";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
|
|
||||||
export type Notification = typeof notifications.$inferSelect;
|
|
||||||
|
|
||||||
export const createSlackNotification = async (
|
|
||||||
input: typeof apiCreateSlack._type,
|
|
||||||
) => {
|
|
||||||
await db.transaction(async (tx) => {
|
|
||||||
const newSlack = await tx
|
|
||||||
.insert(slack)
|
|
||||||
.values({
|
|
||||||
channel: input.channel,
|
|
||||||
webhookUrl: input.webhookUrl,
|
|
||||||
})
|
|
||||||
.returning()
|
|
||||||
.then((value) => value[0]);
|
|
||||||
|
|
||||||
if (!newSlack) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error input: Inserting slack",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const newDestination = await tx
|
|
||||||
.insert(notifications)
|
|
||||||
.values({
|
|
||||||
slackId: newSlack.slackId,
|
|
||||||
name: input.name,
|
|
||||||
appDeploy: input.appDeploy,
|
|
||||||
appBuildError: input.appBuildError,
|
|
||||||
databaseBackup: input.databaseBackup,
|
|
||||||
dokployRestart: input.dokployRestart,
|
|
||||||
dockerCleanup: input.dockerCleanup,
|
|
||||||
notificationType: "slack",
|
|
||||||
})
|
|
||||||
.returning()
|
|
||||||
.then((value) => value[0]);
|
|
||||||
|
|
||||||
if (!newDestination) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error input: Inserting notification",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return newDestination;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateSlackNotification = async (
|
|
||||||
input: typeof apiUpdateSlack._type,
|
|
||||||
) => {
|
|
||||||
await db.transaction(async (tx) => {
|
|
||||||
const newDestination = await tx
|
|
||||||
.update(notifications)
|
|
||||||
.set({
|
|
||||||
name: input.name,
|
|
||||||
appDeploy: input.appDeploy,
|
|
||||||
appBuildError: input.appBuildError,
|
|
||||||
databaseBackup: input.databaseBackup,
|
|
||||||
dokployRestart: input.dokployRestart,
|
|
||||||
dockerCleanup: input.dockerCleanup,
|
|
||||||
})
|
|
||||||
.where(eq(notifications.notificationId, input.notificationId))
|
|
||||||
.returning()
|
|
||||||
.then((value) => value[0]);
|
|
||||||
|
|
||||||
if (!newDestination) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error Updating notification",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await tx
|
|
||||||
.update(slack)
|
|
||||||
.set({
|
|
||||||
channel: input.channel,
|
|
||||||
webhookUrl: input.webhookUrl,
|
|
||||||
})
|
|
||||||
.where(eq(slack.slackId, input.slackId))
|
|
||||||
.returning()
|
|
||||||
.then((value) => value[0]);
|
|
||||||
|
|
||||||
return newDestination;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createTelegramNotification = async (
|
|
||||||
input: typeof apiCreateTelegram._type,
|
|
||||||
) => {
|
|
||||||
await db.transaction(async (tx) => {
|
|
||||||
const newTelegram = await tx
|
|
||||||
.insert(telegram)
|
|
||||||
.values({
|
|
||||||
botToken: input.botToken,
|
|
||||||
chatId: input.chatId,
|
|
||||||
})
|
|
||||||
.returning()
|
|
||||||
.then((value) => value[0]);
|
|
||||||
|
|
||||||
if (!newTelegram) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error input: Inserting telegram",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const newDestination = await tx
|
|
||||||
.insert(notifications)
|
|
||||||
.values({
|
|
||||||
telegramId: newTelegram.telegramId,
|
|
||||||
name: input.name,
|
|
||||||
appDeploy: input.appDeploy,
|
|
||||||
appBuildError: input.appBuildError,
|
|
||||||
databaseBackup: input.databaseBackup,
|
|
||||||
dokployRestart: input.dokployRestart,
|
|
||||||
dockerCleanup: input.dockerCleanup,
|
|
||||||
notificationType: "telegram",
|
|
||||||
})
|
|
||||||
.returning()
|
|
||||||
.then((value) => value[0]);
|
|
||||||
|
|
||||||
if (!newDestination) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error input: Inserting notification",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return newDestination;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateTelegramNotification = async (
|
|
||||||
input: typeof apiUpdateTelegram._type,
|
|
||||||
) => {
|
|
||||||
await db.transaction(async (tx) => {
|
|
||||||
const newDestination = await tx
|
|
||||||
.update(notifications)
|
|
||||||
.set({
|
|
||||||
name: input.name,
|
|
||||||
appDeploy: input.appDeploy,
|
|
||||||
appBuildError: input.appBuildError,
|
|
||||||
databaseBackup: input.databaseBackup,
|
|
||||||
dokployRestart: input.dokployRestart,
|
|
||||||
dockerCleanup: input.dockerCleanup,
|
|
||||||
})
|
|
||||||
.where(eq(notifications.notificationId, input.notificationId))
|
|
||||||
.returning()
|
|
||||||
.then((value) => value[0]);
|
|
||||||
|
|
||||||
if (!newDestination) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error Updating notification",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await tx
|
|
||||||
.update(telegram)
|
|
||||||
.set({
|
|
||||||
botToken: input.botToken,
|
|
||||||
chatId: input.chatId,
|
|
||||||
})
|
|
||||||
.where(eq(telegram.telegramId, input.telegramId))
|
|
||||||
.returning()
|
|
||||||
.then((value) => value[0]);
|
|
||||||
|
|
||||||
return newDestination;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createDiscordNotification = async (
|
|
||||||
input: typeof apiCreateDiscord._type,
|
|
||||||
) => {
|
|
||||||
await db.transaction(async (tx) => {
|
|
||||||
const newDiscord = await tx
|
|
||||||
.insert(discord)
|
|
||||||
.values({
|
|
||||||
webhookUrl: input.webhookUrl,
|
|
||||||
})
|
|
||||||
.returning()
|
|
||||||
.then((value) => value[0]);
|
|
||||||
|
|
||||||
if (!newDiscord) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error input: Inserting discord",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const newDestination = await tx
|
|
||||||
.insert(notifications)
|
|
||||||
.values({
|
|
||||||
discordId: newDiscord.discordId,
|
|
||||||
name: input.name,
|
|
||||||
appDeploy: input.appDeploy,
|
|
||||||
appBuildError: input.appBuildError,
|
|
||||||
databaseBackup: input.databaseBackup,
|
|
||||||
dokployRestart: input.dokployRestart,
|
|
||||||
dockerCleanup: input.dockerCleanup,
|
|
||||||
notificationType: "discord",
|
|
||||||
})
|
|
||||||
.returning()
|
|
||||||
.then((value) => value[0]);
|
|
||||||
|
|
||||||
if (!newDestination) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error input: Inserting notification",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return newDestination;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateDiscordNotification = async (
|
|
||||||
input: typeof apiUpdateDiscord._type,
|
|
||||||
) => {
|
|
||||||
await db.transaction(async (tx) => {
|
|
||||||
const newDestination = await tx
|
|
||||||
.update(notifications)
|
|
||||||
.set({
|
|
||||||
name: input.name,
|
|
||||||
appDeploy: input.appDeploy,
|
|
||||||
appBuildError: input.appBuildError,
|
|
||||||
databaseBackup: input.databaseBackup,
|
|
||||||
dokployRestart: input.dokployRestart,
|
|
||||||
dockerCleanup: input.dockerCleanup,
|
|
||||||
})
|
|
||||||
.where(eq(notifications.notificationId, input.notificationId))
|
|
||||||
.returning()
|
|
||||||
.then((value) => value[0]);
|
|
||||||
|
|
||||||
if (!newDestination) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error Updating notification",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await tx
|
|
||||||
.update(discord)
|
|
||||||
.set({
|
|
||||||
webhookUrl: input.webhookUrl,
|
|
||||||
})
|
|
||||||
.where(eq(discord.discordId, input.discordId))
|
|
||||||
.returning()
|
|
||||||
.then((value) => value[0]);
|
|
||||||
|
|
||||||
return newDestination;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createEmailNotification = async (
|
|
||||||
input: typeof apiCreateEmail._type,
|
|
||||||
) => {
|
|
||||||
await db.transaction(async (tx) => {
|
|
||||||
const newEmail = await tx
|
|
||||||
.insert(email)
|
|
||||||
.values({
|
|
||||||
smtpServer: input.smtpServer,
|
|
||||||
smtpPort: input.smtpPort,
|
|
||||||
username: input.username,
|
|
||||||
password: input.password,
|
|
||||||
fromAddress: input.fromAddress,
|
|
||||||
toAddresses: input.toAddresses,
|
|
||||||
})
|
|
||||||
.returning()
|
|
||||||
.then((value) => value[0]);
|
|
||||||
|
|
||||||
if (!newEmail) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error input: Inserting email",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const newDestination = await tx
|
|
||||||
.insert(notifications)
|
|
||||||
.values({
|
|
||||||
emailId: newEmail.emailId,
|
|
||||||
name: input.name,
|
|
||||||
appDeploy: input.appDeploy,
|
|
||||||
appBuildError: input.appBuildError,
|
|
||||||
databaseBackup: input.databaseBackup,
|
|
||||||
dokployRestart: input.dokployRestart,
|
|
||||||
dockerCleanup: input.dockerCleanup,
|
|
||||||
notificationType: "email",
|
|
||||||
})
|
|
||||||
.returning()
|
|
||||||
.then((value) => value[0]);
|
|
||||||
|
|
||||||
if (!newDestination) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error input: Inserting notification",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return newDestination;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateEmailNotification = async (
|
|
||||||
input: typeof apiUpdateEmail._type,
|
|
||||||
) => {
|
|
||||||
await db.transaction(async (tx) => {
|
|
||||||
const newDestination = await tx
|
|
||||||
.update(notifications)
|
|
||||||
.set({
|
|
||||||
name: input.name,
|
|
||||||
appDeploy: input.appDeploy,
|
|
||||||
appBuildError: input.appBuildError,
|
|
||||||
databaseBackup: input.databaseBackup,
|
|
||||||
dokployRestart: input.dokployRestart,
|
|
||||||
dockerCleanup: input.dockerCleanup,
|
|
||||||
})
|
|
||||||
.where(eq(notifications.notificationId, input.notificationId))
|
|
||||||
.returning()
|
|
||||||
.then((value) => value[0]);
|
|
||||||
|
|
||||||
if (!newDestination) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error Updating notification",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await tx
|
|
||||||
.update(email)
|
|
||||||
.set({
|
|
||||||
smtpServer: input.smtpServer,
|
|
||||||
smtpPort: input.smtpPort,
|
|
||||||
username: input.username,
|
|
||||||
password: input.password,
|
|
||||||
fromAddress: input.fromAddress,
|
|
||||||
toAddresses: input.toAddresses,
|
|
||||||
})
|
|
||||||
.where(eq(email.emailId, input.emailId))
|
|
||||||
.returning()
|
|
||||||
.then((value) => value[0]);
|
|
||||||
|
|
||||||
return newDestination;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findNotificationById = async (notificationId: string) => {
|
|
||||||
const notification = await db.query.notifications.findFirst({
|
|
||||||
where: eq(notifications.notificationId, notificationId),
|
|
||||||
with: {
|
|
||||||
slack: true,
|
|
||||||
telegram: true,
|
|
||||||
discord: true,
|
|
||||||
email: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!notification) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Notification not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return notification;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const removeNotificationById = async (notificationId: string) => {
|
|
||||||
const result = await db
|
|
||||||
.delete(notifications)
|
|
||||||
.where(eq(notifications.notificationId, notificationId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
return result[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateDestinationById = async (
|
|
||||||
notificationId: string,
|
|
||||||
notificationData: Partial<Notification>,
|
|
||||||
) => {
|
|
||||||
const result = await db
|
|
||||||
.update(notifications)
|
|
||||||
.set({
|
|
||||||
...notificationData,
|
|
||||||
})
|
|
||||||
.where(eq(notifications.notificationId, notificationId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
return result[0];
|
|
||||||
};
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import { db } from "@/server/db";
|
|
||||||
import { type apiCreatePort, ports } from "@/server/db/schema";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
|
|
||||||
export type Port = typeof ports.$inferSelect;
|
|
||||||
|
|
||||||
export const createPort = async (input: typeof apiCreatePort._type) => {
|
|
||||||
const newPort = await db
|
|
||||||
.insert(ports)
|
|
||||||
.values({
|
|
||||||
...input,
|
|
||||||
})
|
|
||||||
.returning()
|
|
||||||
.then((value) => value[0]);
|
|
||||||
|
|
||||||
if (!newPort) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error input: Inserting port",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return newPort;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const finPortById = async (portId: string) => {
|
|
||||||
const result = await db.query.ports.findFirst({
|
|
||||||
where: eq(ports.portId, portId),
|
|
||||||
});
|
|
||||||
if (!result) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Port not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const removePortById = async (portId: string) => {
|
|
||||||
const result = await db
|
|
||||||
.delete(ports)
|
|
||||||
.where(eq(ports.portId, portId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
return result[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updatePortById = async (
|
|
||||||
portId: string,
|
|
||||||
portData: Partial<Port>,
|
|
||||||
) => {
|
|
||||||
const result = await db
|
|
||||||
.update(ports)
|
|
||||||
.set({
|
|
||||||
...portData,
|
|
||||||
})
|
|
||||||
.where(eq(ports.portId, portId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
return result[0];
|
|
||||||
};
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
import { generateRandomPassword } from "@/server/auth/random-password";
|
|
||||||
import { db } from "@/server/db";
|
|
||||||
import { type apiCreatePostgres, backups, postgres } from "@/server/db/schema";
|
|
||||||
import { generateAppName } from "@/server/db/schema";
|
|
||||||
import { buildPostgres } from "@/server/utils/databases/postgres";
|
|
||||||
import { pullImage } from "@/server/utils/docker/utils";
|
|
||||||
import { generatePassword } from "@/templates/utils";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import { eq, getTableColumns } from "drizzle-orm";
|
|
||||||
import { validUniqueServerAppName } from "./project";
|
|
||||||
|
|
||||||
import { execAsyncRemote } from "@/server/utils/process/execAsync";
|
|
||||||
|
|
||||||
export type Postgres = typeof postgres.$inferSelect;
|
|
||||||
|
|
||||||
export const createPostgres = async (input: typeof apiCreatePostgres._type) => {
|
|
||||||
input.appName =
|
|
||||||
`${input.appName}-${generatePassword(6)}` || generateAppName("postgres");
|
|
||||||
if (input.appName) {
|
|
||||||
const valid = await validUniqueServerAppName(input.appName);
|
|
||||||
|
|
||||||
if (!valid) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "CONFLICT",
|
|
||||||
message: "Service with this 'AppName' already exists",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const newPostgres = await db
|
|
||||||
.insert(postgres)
|
|
||||||
.values({
|
|
||||||
...input,
|
|
||||||
databasePassword: input.databasePassword
|
|
||||||
? input.databasePassword
|
|
||||||
: (await generateRandomPassword()).randomPassword,
|
|
||||||
})
|
|
||||||
.returning()
|
|
||||||
.then((value) => value[0]);
|
|
||||||
|
|
||||||
if (!newPostgres) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error input: Inserting postgresql database",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return newPostgres;
|
|
||||||
};
|
|
||||||
export const findPostgresById = async (postgresId: string) => {
|
|
||||||
const result = await db.query.postgres.findFirst({
|
|
||||||
where: eq(postgres.postgresId, postgresId),
|
|
||||||
with: {
|
|
||||||
project: true,
|
|
||||||
mounts: true,
|
|
||||||
server: true,
|
|
||||||
backups: {
|
|
||||||
with: {
|
|
||||||
destination: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!result) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Postgres not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findPostgresByBackupId = async (backupId: string) => {
|
|
||||||
const result = await db
|
|
||||||
.select({
|
|
||||||
...getTableColumns(postgres),
|
|
||||||
})
|
|
||||||
.from(postgres)
|
|
||||||
.innerJoin(backups, eq(postgres.postgresId, backups.postgresId))
|
|
||||||
.where(eq(backups.backupId, backupId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!result || !result[0]) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Postgres not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return result[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updatePostgresById = async (
|
|
||||||
postgresId: string,
|
|
||||||
postgresData: Partial<Postgres>,
|
|
||||||
) => {
|
|
||||||
const result = await db
|
|
||||||
.update(postgres)
|
|
||||||
.set({
|
|
||||||
...postgresData,
|
|
||||||
})
|
|
||||||
.where(eq(postgres.postgresId, postgresId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
return result[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const removePostgresById = async (postgresId: string) => {
|
|
||||||
const result = await db
|
|
||||||
.delete(postgres)
|
|
||||||
.where(eq(postgres.postgresId, postgresId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
return result[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deployPostgres = async (postgresId: string) => {
|
|
||||||
const postgres = await findPostgresById(postgresId);
|
|
||||||
try {
|
|
||||||
const promises = [];
|
|
||||||
if (postgres.serverId) {
|
|
||||||
const result = await execAsyncRemote(
|
|
||||||
postgres.serverId,
|
|
||||||
`docker pull ${postgres.dockerImage}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await pullImage(postgres.dockerImage);
|
|
||||||
}
|
|
||||||
|
|
||||||
await buildPostgres(postgres);
|
|
||||||
await updatePostgresById(postgresId, {
|
|
||||||
applicationStatus: "done",
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
await updatePostgresById(postgresId, {
|
|
||||||
applicationStatus: "error",
|
|
||||||
});
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
|
||||||
message: `Error on deploy postgres${error}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return postgres;
|
|
||||||
};
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
import { db } from "@/server/db";
|
|
||||||
import {
|
|
||||||
type apiCreateProject,
|
|
||||||
applications,
|
|
||||||
mariadb,
|
|
||||||
mongo,
|
|
||||||
mysql,
|
|
||||||
postgres,
|
|
||||||
projects,
|
|
||||||
redis,
|
|
||||||
} from "@/server/db/schema";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
|
|
||||||
export type Project = typeof projects.$inferSelect;
|
|
||||||
|
|
||||||
export const createProject = async (
|
|
||||||
input: typeof apiCreateProject._type,
|
|
||||||
adminId: string,
|
|
||||||
) => {
|
|
||||||
const newProject = await db
|
|
||||||
.insert(projects)
|
|
||||||
.values({
|
|
||||||
...input,
|
|
||||||
adminId: adminId,
|
|
||||||
})
|
|
||||||
.returning()
|
|
||||||
.then((value) => value[0]);
|
|
||||||
|
|
||||||
if (!newProject) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error to create the project",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return newProject;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findProjectById = async (projectId: string) => {
|
|
||||||
const project = await db.query.projects.findFirst({
|
|
||||||
where: eq(projects.projectId, projectId),
|
|
||||||
with: {
|
|
||||||
applications: true,
|
|
||||||
mariadb: true,
|
|
||||||
mongo: true,
|
|
||||||
mysql: true,
|
|
||||||
postgres: true,
|
|
||||||
redis: true,
|
|
||||||
compose: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!project) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Project not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return project;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteProject = async (projectId: string) => {
|
|
||||||
const project = await db
|
|
||||||
.delete(projects)
|
|
||||||
.where(eq(projects.projectId, projectId))
|
|
||||||
.returning()
|
|
||||||
.then((value) => value[0]);
|
|
||||||
|
|
||||||
return project;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateProjectById = async (
|
|
||||||
projectId: string,
|
|
||||||
projectData: Partial<Project>,
|
|
||||||
) => {
|
|
||||||
const result = await db
|
|
||||||
.update(projects)
|
|
||||||
.set({
|
|
||||||
...projectData,
|
|
||||||
})
|
|
||||||
.where(eq(projects.projectId, projectId))
|
|
||||||
.returning()
|
|
||||||
.then((res) => res[0]);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const validUniqueServerAppName = async (appName: string) => {
|
|
||||||
const query = await db.query.projects.findMany({
|
|
||||||
with: {
|
|
||||||
applications: {
|
|
||||||
where: eq(applications.appName, appName),
|
|
||||||
},
|
|
||||||
mariadb: {
|
|
||||||
where: eq(mariadb.appName, appName),
|
|
||||||
},
|
|
||||||
mongo: {
|
|
||||||
where: eq(mongo.appName, appName),
|
|
||||||
},
|
|
||||||
mysql: {
|
|
||||||
where: eq(mysql.appName, appName),
|
|
||||||
},
|
|
||||||
postgres: {
|
|
||||||
where: eq(postgres.appName, appName),
|
|
||||||
},
|
|
||||||
redis: {
|
|
||||||
where: eq(redis.appName, appName),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Filter out items with non-empty fields
|
|
||||||
const nonEmptyProjects = query.filter(
|
|
||||||
(project) =>
|
|
||||||
project.applications.length > 0 ||
|
|
||||||
project.mariadb.length > 0 ||
|
|
||||||
project.mongo.length > 0 ||
|
|
||||||
project.mysql.length > 0 ||
|
|
||||||
project.postgres.length > 0 ||
|
|
||||||
project.redis.length > 0,
|
|
||||||
);
|
|
||||||
|
|
||||||
return nonEmptyProjects.length === 0;
|
|
||||||
};
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
import { db } from "@/server/db";
|
|
||||||
import { type apiCreateRedirect, redirects } from "@/server/db/schema";
|
|
||||||
import {
|
|
||||||
createRedirectMiddleware,
|
|
||||||
removeRedirectMiddleware,
|
|
||||||
updateRedirectMiddleware,
|
|
||||||
} from "@/server/utils/traefik/redirect";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import { desc, eq } from "drizzle-orm";
|
|
||||||
import type { z } from "zod";
|
|
||||||
import { findApplicationById } from "./application";
|
|
||||||
export type Redirect = typeof redirects.$inferSelect;
|
|
||||||
|
|
||||||
export const findRedirectById = async (redirectId: string) => {
|
|
||||||
const application = await db.query.redirects.findFirst({
|
|
||||||
where: eq(redirects.redirectId, redirectId),
|
|
||||||
});
|
|
||||||
if (!application) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Redirect not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return application;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createRedirect = async (
|
|
||||||
redirectData: z.infer<typeof apiCreateRedirect>,
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
await db.transaction(async (tx) => {
|
|
||||||
const redirect = await tx
|
|
||||||
.insert(redirects)
|
|
||||||
.values({
|
|
||||||
...redirectData,
|
|
||||||
})
|
|
||||||
.returning()
|
|
||||||
.then((res) => res[0]);
|
|
||||||
|
|
||||||
if (!redirect) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error to create the redirect",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const application = await findApplicationById(redirect.applicationId);
|
|
||||||
|
|
||||||
createRedirectMiddleware(application, redirect);
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error to create this redirect",
|
|
||||||
cause: error,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const removeRedirectById = async (redirectId: string) => {
|
|
||||||
try {
|
|
||||||
const response = await db
|
|
||||||
.delete(redirects)
|
|
||||||
.where(eq(redirects.redirectId, redirectId))
|
|
||||||
.returning()
|
|
||||||
.then((res) => res[0]);
|
|
||||||
|
|
||||||
if (!response) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Redirect not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const application = await findApplicationById(response.applicationId);
|
|
||||||
|
|
||||||
await removeRedirectMiddleware(application, response);
|
|
||||||
|
|
||||||
return response;
|
|
||||||
} catch (error) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error to remove this redirect",
|
|
||||||
cause: error,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateRedirectById = async (
|
|
||||||
redirectId: string,
|
|
||||||
redirectData: Partial<Redirect>,
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
const redirect = await db
|
|
||||||
.update(redirects)
|
|
||||||
.set({
|
|
||||||
...redirectData,
|
|
||||||
})
|
|
||||||
.where(eq(redirects.redirectId, redirectId))
|
|
||||||
.returning()
|
|
||||||
.then((res) => res[0]);
|
|
||||||
|
|
||||||
if (!redirect) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Redirect not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const application = await findApplicationById(redirect.applicationId);
|
|
||||||
|
|
||||||
await updateRedirectMiddleware(application, redirect);
|
|
||||||
|
|
||||||
return redirect;
|
|
||||||
} catch (error) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error to update this redirect",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
import { generateRandomPassword } from "@/server/auth/random-password";
|
|
||||||
import { db } from "@/server/db";
|
|
||||||
import { type apiCreateRedis, redis } from "@/server/db/schema";
|
|
||||||
import { generateAppName } from "@/server/db/schema";
|
|
||||||
import { buildRedis } from "@/server/utils/databases/redis";
|
|
||||||
import { pullImage } from "@/server/utils/docker/utils";
|
|
||||||
import { generatePassword } from "@/templates/utils";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import { validUniqueServerAppName } from "./project";
|
|
||||||
|
|
||||||
import { execAsyncRemote } from "@/server/utils/process/execAsync";
|
|
||||||
|
|
||||||
export type Redis = typeof redis.$inferSelect;
|
|
||||||
|
|
||||||
// https://github.com/drizzle-team/drizzle-orm/discussions/1483#discussioncomment-7523881
|
|
||||||
export const createRedis = async (input: typeof apiCreateRedis._type) => {
|
|
||||||
input.appName =
|
|
||||||
`${input.appName}-${generatePassword(6)}` || generateAppName("redis");
|
|
||||||
if (input.appName) {
|
|
||||||
const valid = await validUniqueServerAppName(input.appName);
|
|
||||||
|
|
||||||
if (!valid) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "CONFLICT",
|
|
||||||
message: "Service with this 'AppName' already exists",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const newRedis = await db
|
|
||||||
.insert(redis)
|
|
||||||
.values({
|
|
||||||
...input,
|
|
||||||
databasePassword: input.databasePassword
|
|
||||||
? input.databasePassword
|
|
||||||
: (await generateRandomPassword()).randomPassword,
|
|
||||||
})
|
|
||||||
.returning()
|
|
||||||
.then((value) => value[0]);
|
|
||||||
|
|
||||||
if (!newRedis) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error input: Inserting redis database",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return newRedis;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findRedisById = async (redisId: string) => {
|
|
||||||
const result = await db.query.redis.findFirst({
|
|
||||||
where: eq(redis.redisId, redisId),
|
|
||||||
with: {
|
|
||||||
project: true,
|
|
||||||
mounts: true,
|
|
||||||
server: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!result) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Redis not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateRedisById = async (
|
|
||||||
redisId: string,
|
|
||||||
redisData: Partial<Redis>,
|
|
||||||
) => {
|
|
||||||
const result = await db
|
|
||||||
.update(redis)
|
|
||||||
.set({
|
|
||||||
...redisData,
|
|
||||||
})
|
|
||||||
.where(eq(redis.redisId, redisId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
return result[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const removeRedisById = async (redisId: string) => {
|
|
||||||
const result = await db
|
|
||||||
.delete(redis)
|
|
||||||
.where(eq(redis.redisId, redisId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
return result[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deployRedis = async (redisId: string) => {
|
|
||||||
const redis = await findRedisById(redisId);
|
|
||||||
try {
|
|
||||||
if (redis.serverId) {
|
|
||||||
await execAsyncRemote(redis.serverId, `docker pull ${redis.dockerImage}`);
|
|
||||||
} else {
|
|
||||||
await pullImage(redis.dockerImage);
|
|
||||||
}
|
|
||||||
|
|
||||||
await buildRedis(redis);
|
|
||||||
await updateRedisById(redisId, {
|
|
||||||
applicationStatus: "done",
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
await updateRedisById(redisId, {
|
|
||||||
applicationStatus: "error",
|
|
||||||
});
|
|
||||||
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
|
||||||
message: `Error on deploy redis${error}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return redis;
|
|
||||||
};
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
import { db } from "@/server/db";
|
|
||||||
import { type apiCreateRegistry, registry } from "@/server/db/schema";
|
|
||||||
import { initializeRegistry } from "@/server/setup/registry-setup";
|
|
||||||
import { removeService } from "@/server/utils/docker/utils";
|
|
||||||
import { execAsync } from "@/server/utils/process/execAsync";
|
|
||||||
import {
|
|
||||||
manageRegistry,
|
|
||||||
removeSelfHostedRegistry,
|
|
||||||
} from "@/server/utils/traefik/registry";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import { findAdmin } from "./admin";
|
|
||||||
|
|
||||||
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",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newRegistry.registryType === "cloud") {
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
import { db } from "@/server/db";
|
|
||||||
import { type apiCreateSecurity, security } from "@/server/db/schema";
|
|
||||||
import {
|
|
||||||
createSecurityMiddleware,
|
|
||||||
removeSecurityMiddleware,
|
|
||||||
} from "@/server/utils/traefik/security";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import type { z } from "zod";
|
|
||||||
import { findApplicationById } from "./application";
|
|
||||||
export type Security = typeof security.$inferSelect;
|
|
||||||
|
|
||||||
export const findSecurityById = async (securityId: string) => {
|
|
||||||
const application = await db.query.security.findFirst({
|
|
||||||
where: eq(security.securityId, securityId),
|
|
||||||
});
|
|
||||||
if (!application) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Security not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return application;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createSecurity = async (
|
|
||||||
data: z.infer<typeof apiCreateSecurity>,
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
await db.transaction(async (tx) => {
|
|
||||||
const application = await findApplicationById(data.applicationId);
|
|
||||||
|
|
||||||
const securityResponse = await tx
|
|
||||||
.insert(security)
|
|
||||||
.values({
|
|
||||||
...data,
|
|
||||||
})
|
|
||||||
.returning()
|
|
||||||
.then((res) => res[0]);
|
|
||||||
|
|
||||||
if (!securityResponse) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error to create the security",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await createSecurityMiddleware(application, securityResponse);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error to create this security",
|
|
||||||
cause: error,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteSecurityById = async (securityId: string) => {
|
|
||||||
try {
|
|
||||||
const result = await db
|
|
||||||
.delete(security)
|
|
||||||
.where(eq(security.securityId, securityId))
|
|
||||||
.returning()
|
|
||||||
.then((res) => res[0]);
|
|
||||||
|
|
||||||
if (!result) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Security not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const application = await findApplicationById(result.applicationId);
|
|
||||||
|
|
||||||
await removeSecurityMiddleware(application, result);
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error to remove this security",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateSecurityById = async (
|
|
||||||
securityId: string,
|
|
||||||
data: Partial<Security>,
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
const response = await db
|
|
||||||
.update(security)
|
|
||||||
.set({
|
|
||||||
...data,
|
|
||||||
})
|
|
||||||
.where(eq(security.securityId, securityId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
return response[0];
|
|
||||||
} catch (error) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error to update this security",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
import { db } from "@/server/db";
|
|
||||||
|
|
||||||
import { type apiCreateServer, server } from "@/server/db/schema";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import { desc, eq } from "drizzle-orm";
|
|
||||||
|
|
||||||
export type Server = typeof server.$inferSelect;
|
|
||||||
|
|
||||||
export const createServer = async (
|
|
||||||
input: typeof apiCreateServer._type,
|
|
||||||
adminId: string,
|
|
||||||
) => {
|
|
||||||
const newServer = await db
|
|
||||||
.insert(server)
|
|
||||||
.values({
|
|
||||||
...input,
|
|
||||||
adminId: adminId,
|
|
||||||
})
|
|
||||||
.returning()
|
|
||||||
.then((value) => value[0]);
|
|
||||||
|
|
||||||
if (!newServer) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error to create the server",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return newServer;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findServerById = async (serverId: string) => {
|
|
||||||
const currentServer = await db.query.server.findFirst({
|
|
||||||
where: eq(server.serverId, serverId),
|
|
||||||
with: {
|
|
||||||
deployments: true,
|
|
||||||
sshKey: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!currentServer) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Server not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return currentServer;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findServersByAdminId = async (adminId: string) => {
|
|
||||||
const servers = await db.query.server.findMany({
|
|
||||||
where: eq(server.adminId, adminId),
|
|
||||||
orderBy: desc(server.createdAt),
|
|
||||||
});
|
|
||||||
|
|
||||||
return servers;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteServer = async (serverId: string) => {
|
|
||||||
const currentServer = await db
|
|
||||||
.delete(server)
|
|
||||||
.where(eq(server.serverId, serverId))
|
|
||||||
.returning()
|
|
||||||
.then((value) => value[0]);
|
|
||||||
|
|
||||||
return currentServer;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const haveActiveServices = async (serverId: string) => {
|
|
||||||
const currentServer = await db.query.server.findFirst({
|
|
||||||
where: eq(server.serverId, serverId),
|
|
||||||
with: {
|
|
||||||
applications: true,
|
|
||||||
compose: true,
|
|
||||||
redis: true,
|
|
||||||
mariadb: true,
|
|
||||||
mongo: true,
|
|
||||||
mysql: true,
|
|
||||||
postgres: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!currentServer) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const total =
|
|
||||||
currentServer?.applications?.length +
|
|
||||||
currentServer?.compose?.length +
|
|
||||||
currentServer?.redis?.length +
|
|
||||||
currentServer?.mariadb?.length +
|
|
||||||
currentServer?.mongo?.length +
|
|
||||||
currentServer?.mysql?.length +
|
|
||||||
currentServer?.postgres?.length;
|
|
||||||
|
|
||||||
if (total === 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateServerById = async (
|
|
||||||
serverId: string,
|
|
||||||
serverData: Partial<Server>,
|
|
||||||
) => {
|
|
||||||
const result = await db
|
|
||||||
.update(server)
|
|
||||||
.set({
|
|
||||||
...serverData,
|
|
||||||
})
|
|
||||||
.where(eq(server.serverId, serverId))
|
|
||||||
.returning()
|
|
||||||
.then((res) => res[0]);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getAllServers = async () => {
|
|
||||||
const servers = await db.query.server.findMany();
|
|
||||||
return servers;
|
|
||||||
};
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
import { readdirSync } from "node:fs";
|
|
||||||
import { join } from "node:path";
|
|
||||||
import { docker } from "@/server/constants";
|
|
||||||
import { getServiceContainer } from "@/server/utils/docker/utils";
|
|
||||||
import { execAsyncRemote } from "@/server/utils/process/execAsync";
|
|
||||||
import packageInfo from "../../../package.json";
|
|
||||||
|
|
||||||
const updateIsAvailable = async () => {
|
|
||||||
try {
|
|
||||||
const service = await getServiceContainer("dokploy");
|
|
||||||
|
|
||||||
const localImage = await docker.getImage(getDokployImage()).inspect();
|
|
||||||
return localImage.Id !== service?.ImageID;
|
|
||||||
} catch (error) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getDokployImage = () => {
|
|
||||||
return `dokploy/dokploy:${process.env.RELEASE_TAG || "latest"}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const pullLatestRelease = async () => {
|
|
||||||
try {
|
|
||||||
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) {}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
export const getDokployVersion = () => {
|
|
||||||
return packageInfo.version;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface TreeDataItem {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
type: "file" | "directory";
|
|
||||||
children?: TreeDataItem[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const readDirectory = async (
|
|
||||||
dirPath: string,
|
|
||||||
serverId?: string,
|
|
||||||
): Promise<TreeDataItem[]> => {
|
|
||||||
if (serverId) {
|
|
||||||
const { stdout } = await execAsyncRemote(
|
|
||||||
serverId,
|
|
||||||
`
|
|
||||||
process_items() {
|
|
||||||
local parent_dir="$1"
|
|
||||||
local __resultvar=$2
|
|
||||||
|
|
||||||
local items_json=""
|
|
||||||
local first=true
|
|
||||||
for item in "$parent_dir"/*; do
|
|
||||||
[ -e "$item" ] || continue
|
|
||||||
process_item "$item" item_json
|
|
||||||
if [ "$first" = true ]; then
|
|
||||||
first=false
|
|
||||||
items_json="$item_json"
|
|
||||||
else
|
|
||||||
items_json="$items_json,$item_json"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
eval $__resultvar="'[$items_json]'"
|
|
||||||
}
|
|
||||||
|
|
||||||
process_item() {
|
|
||||||
local item_path="$1"
|
|
||||||
local __resultvar=$2
|
|
||||||
|
|
||||||
local item_name=$(basename "$item_path")
|
|
||||||
local escaped_name=$(echo "$item_name" | sed 's/"/\\"/g')
|
|
||||||
local escaped_path=$(echo "$item_path" | sed 's/"/\\"/g')
|
|
||||||
|
|
||||||
if [ -d "$item_path" ]; then
|
|
||||||
# Is directory
|
|
||||||
process_items "$item_path" children_json
|
|
||||||
local json='{"id":"'"$escaped_path"'","name":"'"$escaped_name"'","type":"directory","children":'"$children_json"'}'
|
|
||||||
else
|
|
||||||
# Is file
|
|
||||||
local json='{"id":"'"$escaped_path"'","name":"'"$escaped_name"'","type":"file"}'
|
|
||||||
fi
|
|
||||||
|
|
||||||
eval $__resultvar="'$json'"
|
|
||||||
}
|
|
||||||
|
|
||||||
root_dir=${dirPath}
|
|
||||||
|
|
||||||
process_items "$root_dir" json_output
|
|
||||||
|
|
||||||
echo "$json_output"
|
|
||||||
`,
|
|
||||||
);
|
|
||||||
const result = JSON.parse(stdout);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
const items = readdirSync(dirPath, { withFileTypes: true });
|
|
||||||
|
|
||||||
const stack = [dirPath];
|
|
||||||
const result: TreeDataItem[] = [];
|
|
||||||
const parentMap: Record<string, TreeDataItem[]> = {};
|
|
||||||
|
|
||||||
while (stack.length > 0) {
|
|
||||||
const currentPath = stack.pop();
|
|
||||||
if (!currentPath) continue;
|
|
||||||
|
|
||||||
const items = readdirSync(currentPath, { withFileTypes: true });
|
|
||||||
const currentDirectoryResult: TreeDataItem[] = [];
|
|
||||||
|
|
||||||
for (const item of items) {
|
|
||||||
const fullPath = join(currentPath, item.name);
|
|
||||||
if (item.isDirectory()) {
|
|
||||||
stack.push(fullPath);
|
|
||||||
const directoryItem: TreeDataItem = {
|
|
||||||
id: fullPath,
|
|
||||||
name: item.name,
|
|
||||||
type: "directory",
|
|
||||||
children: [],
|
|
||||||
};
|
|
||||||
currentDirectoryResult.push(directoryItem);
|
|
||||||
parentMap[fullPath] = directoryItem.children as TreeDataItem[];
|
|
||||||
} else {
|
|
||||||
const fileItem: TreeDataItem = {
|
|
||||||
id: fullPath,
|
|
||||||
name: item.name,
|
|
||||||
type: "file",
|
|
||||||
};
|
|
||||||
currentDirectoryResult.push(fileItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parentMap[currentPath]) {
|
|
||||||
parentMap[currentPath].push(...currentDirectoryResult);
|
|
||||||
} else {
|
|
||||||
result.push(...currentDirectoryResult);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import { db } from "@/server/db";
|
|
||||||
import {
|
|
||||||
type apiCreateSshKey,
|
|
||||||
type apiFindOneSshKey,
|
|
||||||
type apiRemoveSshKey,
|
|
||||||
type apiUpdateSshKey,
|
|
||||||
sshKeys,
|
|
||||||
} from "@/server/db/schema";
|
|
||||||
import { removeSSHKey, saveSSHKey } from "@/server/utils/filesystem/ssh";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
|
|
||||||
export const createSshKey = async ({
|
|
||||||
privateKey,
|
|
||||||
...input
|
|
||||||
}: typeof apiCreateSshKey._type) => {
|
|
||||||
await db.transaction(async (tx) => {
|
|
||||||
const sshKey = await tx
|
|
||||||
.insert(sshKeys)
|
|
||||||
.values(input)
|
|
||||||
.returning()
|
|
||||||
.then((response) => response[0])
|
|
||||||
.catch((e) => console.error(e));
|
|
||||||
|
|
||||||
if (sshKey) {
|
|
||||||
saveSSHKey(sshKey.sshKeyId, sshKey.publicKey, privateKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sshKey) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error to create the ssh key",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return sshKey;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const removeSSHKeyById = async (
|
|
||||||
sshKeyId: (typeof apiRemoveSshKey._type)["sshKeyId"],
|
|
||||||
) => {
|
|
||||||
const result = await db
|
|
||||||
.delete(sshKeys)
|
|
||||||
.where(eq(sshKeys.sshKeyId, sshKeyId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
removeSSHKey(sshKeyId);
|
|
||||||
|
|
||||||
return result[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateSSHKeyById = async ({
|
|
||||||
sshKeyId,
|
|
||||||
...input
|
|
||||||
}: typeof apiUpdateSshKey._type) => {
|
|
||||||
const result = await db
|
|
||||||
.update(sshKeys)
|
|
||||||
.set(input)
|
|
||||||
.where(eq(sshKeys.sshKeyId, sshKeyId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
return result[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findSSHKeyById = async (
|
|
||||||
sshKeyId: (typeof apiFindOneSshKey._type)["sshKeyId"],
|
|
||||||
) => {
|
|
||||||
const sshKey = await db.query.sshKeys.findFirst({
|
|
||||||
where: eq(sshKeys.sshKeyId, sshKeyId),
|
|
||||||
});
|
|
||||||
if (!sshKey) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "SSH Key not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return sshKey;
|
|
||||||
};
|
|
||||||
@@ -1,207 +0,0 @@
|
|||||||
import { db } from "@/server/db";
|
|
||||||
import { users } from "@/server/db/schema";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
|
|
||||||
export type User = typeof users.$inferSelect;
|
|
||||||
|
|
||||||
export const findUserById = async (userId: string) => {
|
|
||||||
const user = await db.query.users.findFirst({
|
|
||||||
where: eq(users.userId, userId),
|
|
||||||
});
|
|
||||||
if (!user) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "User not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return user;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findUserByAuthId = async (authId: string) => {
|
|
||||||
const user = await db.query.users.findFirst({
|
|
||||||
where: eq(users.authId, authId),
|
|
||||||
with: {
|
|
||||||
auth: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!user) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "User not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return user;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findUsers = async () => {
|
|
||||||
const users = await db.query.users.findMany({
|
|
||||||
with: {
|
|
||||||
auth: {
|
|
||||||
columns: {
|
|
||||||
secret: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return users;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const addNewProject = async (authId: string, projectId: string) => {
|
|
||||||
const user = await findUserByAuthId(authId);
|
|
||||||
|
|
||||||
await db
|
|
||||||
.update(users)
|
|
||||||
.set({
|
|
||||||
accesedProjects: [...user.accesedProjects, projectId],
|
|
||||||
})
|
|
||||||
.where(eq(users.authId, authId));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const addNewService = async (authId: string, serviceId: string) => {
|
|
||||||
const user = await findUserByAuthId(authId);
|
|
||||||
await db
|
|
||||||
.update(users)
|
|
||||||
.set({
|
|
||||||
accesedServices: [...user.accesedServices, serviceId],
|
|
||||||
})
|
|
||||||
.where(eq(users.authId, authId));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const canPerformCreationService = async (
|
|
||||||
userId: string,
|
|
||||||
projectId: string,
|
|
||||||
) => {
|
|
||||||
const { accesedProjects, canCreateServices } = await findUserByAuthId(userId);
|
|
||||||
const haveAccessToProject = accesedProjects.includes(projectId);
|
|
||||||
|
|
||||||
if (canCreateServices && haveAccessToProject) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const canPerformAccessService = async (
|
|
||||||
userId: string,
|
|
||||||
serviceId: string,
|
|
||||||
) => {
|
|
||||||
const { accesedServices } = await findUserByAuthId(userId);
|
|
||||||
const haveAccessToService = accesedServices.includes(serviceId);
|
|
||||||
|
|
||||||
if (haveAccessToService) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const canPeformDeleteService = async (
|
|
||||||
authId: string,
|
|
||||||
serviceId: string,
|
|
||||||
) => {
|
|
||||||
const { accesedServices, canDeleteServices } = await findUserByAuthId(authId);
|
|
||||||
const haveAccessToService = accesedServices.includes(serviceId);
|
|
||||||
|
|
||||||
if (canDeleteServices && haveAccessToService) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const canPerformCreationProject = async (authId: string) => {
|
|
||||||
const { canCreateProjects } = await findUserByAuthId(authId);
|
|
||||||
|
|
||||||
if (canCreateProjects) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const canPerformDeleteProject = async (authId: string) => {
|
|
||||||
const { canDeleteProjects } = await findUserByAuthId(authId);
|
|
||||||
|
|
||||||
if (canDeleteProjects) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const canPerformAccessProject = async (
|
|
||||||
authId: string,
|
|
||||||
projectId: string,
|
|
||||||
) => {
|
|
||||||
const { accesedProjects } = await findUserByAuthId(authId);
|
|
||||||
|
|
||||||
const haveAccessToProject = accesedProjects.includes(projectId);
|
|
||||||
|
|
||||||
if (haveAccessToProject) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const canAccessToTraefikFiles = async (authId: string) => {
|
|
||||||
const { canAccessToTraefikFiles } = await findUserByAuthId(authId);
|
|
||||||
return canAccessToTraefikFiles;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const checkServiceAccess = async (
|
|
||||||
authId: string,
|
|
||||||
serviceId: string,
|
|
||||||
action = "access" as "access" | "create" | "delete",
|
|
||||||
) => {
|
|
||||||
let hasPermission = false;
|
|
||||||
switch (action) {
|
|
||||||
case "create":
|
|
||||||
hasPermission = await canPerformCreationService(authId, serviceId);
|
|
||||||
break;
|
|
||||||
case "access":
|
|
||||||
hasPermission = await canPerformAccessService(authId, serviceId);
|
|
||||||
break;
|
|
||||||
case "delete":
|
|
||||||
hasPermission = await canPeformDeleteService(authId, serviceId);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
hasPermission = false;
|
|
||||||
}
|
|
||||||
if (!hasPermission) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "Permission denied",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const checkProjectAccess = async (
|
|
||||||
authId: string,
|
|
||||||
action: "create" | "delete" | "access",
|
|
||||||
projectId?: string,
|
|
||||||
) => {
|
|
||||||
let hasPermission = false;
|
|
||||||
switch (action) {
|
|
||||||
case "access":
|
|
||||||
hasPermission = await canPerformAccessProject(
|
|
||||||
authId,
|
|
||||||
projectId as string,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case "create":
|
|
||||||
hasPermission = await canPerformCreationProject(authId);
|
|
||||||
break;
|
|
||||||
case "delete":
|
|
||||||
hasPermission = await canPerformDeleteProject(authId);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
hasPermission = false;
|
|
||||||
}
|
|
||||||
if (!hasPermission) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "Permission denied",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -4,8 +4,7 @@ import { DrizzlePostgreSQLAdapter } from "@lucia-auth/adapter-drizzle";
|
|||||||
import { TimeSpan } from "lucia";
|
import { TimeSpan } from "lucia";
|
||||||
import { Lucia } from "lucia/dist/core.js";
|
import { Lucia } from "lucia/dist/core.js";
|
||||||
import type { Session, User } from "lucia/dist/core.js";
|
import type { Session, User } from "lucia/dist/core.js";
|
||||||
import { findAdminByAuthId } from "../api/services/admin";
|
import { findAdminByAuthId, findUserByAuthId } from "@dokploy/builders";
|
||||||
import { findUserByAuthId } from "../api/services/user";
|
|
||||||
import { db } from "../db";
|
import { db } from "../db";
|
||||||
import { type DatabaseUser, auth, sessionTable } from "../db/schema";
|
import { type DatabaseUser, auth, sessionTable } from "../db/schema";
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,23 @@
|
|||||||
import { type Job, Worker } from "bullmq";
|
import { type Job, Worker } from "bullmq";
|
||||||
|
// import {
|
||||||
|
// deployApplication,
|
||||||
|
// deployRemoteApplication,
|
||||||
|
// rebuildApplication,
|
||||||
|
// rebuildRemoteApplication,
|
||||||
|
// updateApplicationStatus,
|
||||||
|
// } from "../api/services/application";
|
||||||
import {
|
import {
|
||||||
deployApplication,
|
deployApplication,
|
||||||
deployRemoteApplication,
|
deployRemoteApplication,
|
||||||
rebuildApplication,
|
rebuildApplication,
|
||||||
rebuildRemoteApplication,
|
rebuildRemoteApplication,
|
||||||
updateApplicationStatus,
|
updateApplicationStatus,
|
||||||
} from "../api/services/application";
|
|
||||||
import {
|
|
||||||
deployCompose,
|
deployCompose,
|
||||||
deployRemoteCompose,
|
deployRemoteCompose,
|
||||||
rebuildCompose,
|
rebuildCompose,
|
||||||
rebuildRemoteCompose,
|
rebuildRemoteCompose,
|
||||||
updateCompose,
|
updateCompose,
|
||||||
} from "../api/services/compose";
|
} from "@dokploy/builders";
|
||||||
import { myQueue, redisConfig } from "./queueSetup";
|
import { myQueue, redisConfig } from "./queueSetup";
|
||||||
|
|
||||||
type DeployJob =
|
type DeployJob =
|
||||||
|
|||||||
@@ -4,18 +4,17 @@ import { config } from "dotenv";
|
|||||||
import next from "next";
|
import next from "next";
|
||||||
// import { IS_CLOUD } from "./constants";
|
// import { IS_CLOUD } from "./constants";
|
||||||
import { deploymentWorker } from "./queues/deployments-queue";
|
import { deploymentWorker } from "./queues/deployments-queue";
|
||||||
// import { deploymentWorker } from "./queues/deployments-queue";
|
|
||||||
import { setupDirectories } from "./setup/config-paths";
|
|
||||||
import { initializePostgres } from "./setup/postgres-setup";
|
|
||||||
import { initializeRedis } from "./setup/redis-setup";
|
|
||||||
import { initializeNetwork } from "./setup/setup";
|
|
||||||
import {
|
import {
|
||||||
|
setupDirectories,
|
||||||
|
initializePostgres,
|
||||||
|
initializeRedis,
|
||||||
|
initializeNetwork,
|
||||||
createDefaultMiddlewares,
|
createDefaultMiddlewares,
|
||||||
createDefaultServerTraefikConfig,
|
createDefaultServerTraefikConfig,
|
||||||
createDefaultTraefikConfig,
|
createDefaultTraefikConfig,
|
||||||
initializeTraefik,
|
initializeTraefik,
|
||||||
} from "./setup/traefik-setup";
|
initCronJobs,
|
||||||
import { initCronJobs } from "./utils/backups";
|
} from "@dokploy/builders";
|
||||||
import { sendDokployRestartNotifications } from "./utils/notifications/dokploy-restart";
|
import { sendDokployRestartNotifications } from "./utils/notifications/dokploy-restart";
|
||||||
import { setupDockerContainerLogsWebSocketServer } from "./wss/docker-container-logs";
|
import { setupDockerContainerLogsWebSocketServer } from "./wss/docker-container-logs";
|
||||||
import { setupDockerContainerTerminalWebSocketServer } from "./wss/docker-container-terminal";
|
import { setupDockerContainerTerminalWebSocketServer } from "./wss/docker-container-terminal";
|
||||||
|
|||||||
@@ -1,157 +0,0 @@
|
|||||||
import { findAdmin } from "@/server/api/services/admin";
|
|
||||||
import { getAllServers } from "@/server/api/services/server";
|
|
||||||
import { scheduleJob } from "node-schedule";
|
|
||||||
import { db } from "../../db/index";
|
|
||||||
import {
|
|
||||||
cleanUpDockerBuilder,
|
|
||||||
cleanUpSystemPrune,
|
|
||||||
cleanUpUnusedImages,
|
|
||||||
} from "../docker/utils";
|
|
||||||
import { runMariadbBackup } from "./mariadb";
|
|
||||||
import { runMongoBackup } from "./mongo";
|
|
||||||
import { runMySqlBackup } from "./mysql";
|
|
||||||
import { runPostgresBackup } from "./postgres";
|
|
||||||
|
|
||||||
export const initCronJobs = async () => {
|
|
||||||
console.log("Setting up cron jobs....");
|
|
||||||
|
|
||||||
const admin = await findAdmin();
|
|
||||||
|
|
||||||
if (admin?.enableDockerCleanup) {
|
|
||||||
scheduleJob("docker-cleanup", "0 0 * * *", async () => {
|
|
||||||
console.log(
|
|
||||||
`Docker Cleanup ${new Date().toLocaleString()}] Running docker cleanup`,
|
|
||||||
);
|
|
||||||
await cleanUpUnusedImages();
|
|
||||||
await cleanUpDockerBuilder();
|
|
||||||
await cleanUpSystemPrune();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const servers = await getAllServers();
|
|
||||||
|
|
||||||
for (const server of servers) {
|
|
||||||
const { appName, serverId } = server;
|
|
||||||
if (serverId) {
|
|
||||||
scheduleJob(serverId, "0 0 * * *", async () => {
|
|
||||||
console.log(
|
|
||||||
`SERVER-BACKUP[${new Date().toLocaleString()}] Running Cleanup ${appName}`,
|
|
||||||
);
|
|
||||||
await cleanUpUnusedImages(serverId);
|
|
||||||
await cleanUpDockerBuilder(serverId);
|
|
||||||
await cleanUpSystemPrune(serverId);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const pgs = await db.query.postgres.findMany({
|
|
||||||
with: {
|
|
||||||
backups: {
|
|
||||||
with: {
|
|
||||||
destination: true,
|
|
||||||
postgres: true,
|
|
||||||
mariadb: true,
|
|
||||||
mysql: true,
|
|
||||||
mongo: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
for (const pg of pgs) {
|
|
||||||
for (const backup of pg.backups) {
|
|
||||||
const { schedule, backupId, enabled } = backup;
|
|
||||||
if (enabled) {
|
|
||||||
scheduleJob(backupId, schedule, async () => {
|
|
||||||
console.log(
|
|
||||||
`PG-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`,
|
|
||||||
);
|
|
||||||
runPostgresBackup(pg, backup);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mariadbs = await db.query.mariadb.findMany({
|
|
||||||
with: {
|
|
||||||
backups: {
|
|
||||||
with: {
|
|
||||||
destination: true,
|
|
||||||
postgres: true,
|
|
||||||
mariadb: true,
|
|
||||||
mysql: true,
|
|
||||||
mongo: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const maria of mariadbs) {
|
|
||||||
for (const backup of maria.backups) {
|
|
||||||
const { schedule, backupId, enabled } = backup;
|
|
||||||
if (enabled) {
|
|
||||||
scheduleJob(backupId, schedule, async () => {
|
|
||||||
console.log(
|
|
||||||
`MARIADB-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`,
|
|
||||||
);
|
|
||||||
await runMariadbBackup(maria, backup);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mongodbs = await db.query.mongo.findMany({
|
|
||||||
with: {
|
|
||||||
backups: {
|
|
||||||
with: {
|
|
||||||
destination: true,
|
|
||||||
postgres: true,
|
|
||||||
mariadb: true,
|
|
||||||
mysql: true,
|
|
||||||
mongo: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const mongo of mongodbs) {
|
|
||||||
for (const backup of mongo.backups) {
|
|
||||||
const { schedule, backupId, enabled } = backup;
|
|
||||||
if (enabled) {
|
|
||||||
scheduleJob(backupId, schedule, async () => {
|
|
||||||
console.log(
|
|
||||||
`MONGO-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`,
|
|
||||||
);
|
|
||||||
await runMongoBackup(mongo, backup);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mysqls = await db.query.mysql.findMany({
|
|
||||||
with: {
|
|
||||||
backups: {
|
|
||||||
with: {
|
|
||||||
destination: true,
|
|
||||||
postgres: true,
|
|
||||||
mariadb: true,
|
|
||||||
mysql: true,
|
|
||||||
mongo: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const mysql of mysqls) {
|
|
||||||
for (const backup of mysql.backups) {
|
|
||||||
const { schedule, backupId, enabled } = backup;
|
|
||||||
if (enabled) {
|
|
||||||
scheduleJob(backupId, schedule, async () => {
|
|
||||||
console.log(
|
|
||||||
`MYSQL-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`,
|
|
||||||
);
|
|
||||||
await runMySqlBackup(mysql, backup);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
import path from "node:path";
|
|
||||||
import type { BackupSchedule } from "@/server/api/services/backup";
|
|
||||||
import type { Mariadb } from "@/server/api/services/mariadb";
|
|
||||||
import { findProjectById } from "@/server/api/services/project";
|
|
||||||
import {
|
|
||||||
getRemoteServiceContainer,
|
|
||||||
getServiceContainer,
|
|
||||||
} from "../docker/utils";
|
|
||||||
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
|
||||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
|
||||||
import { getS3Credentials } from "./utils";
|
|
||||||
|
|
||||||
export const runMariadbBackup = async (
|
|
||||||
mariadb: Mariadb,
|
|
||||||
backup: BackupSchedule,
|
|
||||||
) => {
|
|
||||||
const { appName, databasePassword, databaseUser, projectId, name } = mariadb;
|
|
||||||
const project = await findProjectById(projectId);
|
|
||||||
const { prefix, database } = backup;
|
|
||||||
const destination = backup.destination;
|
|
||||||
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
|
||||||
const bucketDestination = path.join(prefix, backupFileName);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const rcloneFlags = getS3Credentials(destination);
|
|
||||||
const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`;
|
|
||||||
|
|
||||||
const rcloneCommand = `rclone rcat ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
|
|
||||||
if (mariadb.serverId) {
|
|
||||||
const { Id: containerId } = await getRemoteServiceContainer(
|
|
||||||
mariadb.serverId,
|
|
||||||
appName,
|
|
||||||
);
|
|
||||||
const mariadbDumpCommand = `docker exec ${containerId} sh -c "mariadb-dump --user='${databaseUser}' --password='${databasePassword}' --databases ${database} | gzip"`;
|
|
||||||
|
|
||||||
await execAsyncRemote(
|
|
||||||
mariadb.serverId,
|
|
||||||
`${mariadbDumpCommand} | ${rcloneCommand}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const { Id: containerId } = await getServiceContainer(appName);
|
|
||||||
const mariadbDumpCommand = `docker exec ${containerId} sh -c "mariadb-dump --user='${databaseUser}' --password='${databasePassword}' --databases ${database} | gzip"`;
|
|
||||||
|
|
||||||
await execAsync(`${mariadbDumpCommand} | ${rcloneCommand}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await sendDatabaseBackupNotifications({
|
|
||||||
applicationName: name,
|
|
||||||
projectName: project.name,
|
|
||||||
databaseType: "mariadb",
|
|
||||||
type: "success",
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
await sendDatabaseBackupNotifications({
|
|
||||||
applicationName: name,
|
|
||||||
projectName: project.name,
|
|
||||||
databaseType: "mariadb",
|
|
||||||
type: "error",
|
|
||||||
// @ts-ignore
|
|
||||||
errorMessage: error?.message || "Error message not provided",
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import path from "node:path";
|
|
||||||
import type { BackupSchedule } from "@/server/api/services/backup";
|
|
||||||
import type { Mongo } from "@/server/api/services/mongo";
|
|
||||||
import { findProjectById } from "@/server/api/services/project";
|
|
||||||
import {
|
|
||||||
getRemoteServiceContainer,
|
|
||||||
getServiceContainer,
|
|
||||||
} from "../docker/utils";
|
|
||||||
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
|
||||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
|
||||||
import { getS3Credentials } from "./utils";
|
|
||||||
|
|
||||||
// mongodb://mongo:Bqh7AQl-PRbnBu@localhost:27017/?tls=false&directConnection=true
|
|
||||||
export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
|
|
||||||
const { appName, databasePassword, databaseUser, projectId, name } = mongo;
|
|
||||||
const project = await findProjectById(projectId);
|
|
||||||
const { prefix, database } = backup;
|
|
||||||
const destination = backup.destination;
|
|
||||||
const backupFileName = `${new Date().toISOString()}.dump.gz`;
|
|
||||||
const bucketDestination = path.join(prefix, backupFileName);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const rcloneFlags = getS3Credentials(destination);
|
|
||||||
const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`;
|
|
||||||
|
|
||||||
const rcloneCommand = `rclone rcat ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
|
|
||||||
if (mongo.serverId) {
|
|
||||||
const { Id: containerId } = await getRemoteServiceContainer(
|
|
||||||
mongo.serverId,
|
|
||||||
appName,
|
|
||||||
);
|
|
||||||
const mongoDumpCommand = `docker exec ${containerId} sh -c "mongodump -d '${database}' -u '${databaseUser}' -p '${databasePassword}' --authenticationDatabase=admin --gzip"`;
|
|
||||||
|
|
||||||
await execAsyncRemote(
|
|
||||||
mongo.serverId,
|
|
||||||
`${mongoDumpCommand} | ${rcloneCommand}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const { Id: containerId } = await getServiceContainer(appName);
|
|
||||||
const mongoDumpCommand = `docker exec ${containerId} sh -c "mongodump -d '${database}' -u '${databaseUser}' -p '${databasePassword}' --authenticationDatabase=admin --gzip"`;
|
|
||||||
await execAsync(`${mongoDumpCommand} | ${rcloneCommand}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await sendDatabaseBackupNotifications({
|
|
||||||
applicationName: name,
|
|
||||||
projectName: project.name,
|
|
||||||
databaseType: "mongodb",
|
|
||||||
type: "success",
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
await sendDatabaseBackupNotifications({
|
|
||||||
applicationName: name,
|
|
||||||
projectName: project.name,
|
|
||||||
databaseType: "mongodb",
|
|
||||||
type: "error",
|
|
||||||
// @ts-ignore
|
|
||||||
errorMessage: error?.message || "Error message not provided",
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// mongorestore -d monguito -u mongo -p Bqh7AQl-PRbnBu --authenticationDatabase admin --gzip --archive=2024-04-13T05:03:58.937Z.dump.gz
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import { unlink } from "node:fs/promises";
|
|
||||||
import path from "node:path";
|
|
||||||
import type { BackupSchedule } from "@/server/api/services/backup";
|
|
||||||
import type { MySql } from "@/server/api/services/mysql";
|
|
||||||
import { findProjectById } from "@/server/api/services/project";
|
|
||||||
import {
|
|
||||||
getRemoteServiceContainer,
|
|
||||||
getServiceContainer,
|
|
||||||
} from "../docker/utils";
|
|
||||||
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
|
||||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
|
||||||
import { getS3Credentials } from "./utils";
|
|
||||||
|
|
||||||
export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => {
|
|
||||||
const { appName, databaseRootPassword, projectId, name } = mysql;
|
|
||||||
const project = await findProjectById(projectId);
|
|
||||||
const { prefix, database } = backup;
|
|
||||||
const destination = backup.destination;
|
|
||||||
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
|
||||||
const bucketDestination = path.join(prefix, backupFileName);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const rcloneFlags = getS3Credentials(destination);
|
|
||||||
const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`;
|
|
||||||
|
|
||||||
const rcloneCommand = `rclone rcat ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
|
|
||||||
if (mysql.serverId) {
|
|
||||||
const { Id: containerId } = await getRemoteServiceContainer(
|
|
||||||
mysql.serverId,
|
|
||||||
appName,
|
|
||||||
);
|
|
||||||
const mysqlDumpCommand = `docker exec ${containerId} sh -c "mysqldump --default-character-set=utf8mb4 -u 'root' --password='${databaseRootPassword}' --single-transaction --no-tablespaces --quick '${database}' | gzip"`;
|
|
||||||
|
|
||||||
await execAsyncRemote(
|
|
||||||
mysql.serverId,
|
|
||||||
`${mysqlDumpCommand} | ${rcloneCommand}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const { Id: containerId } = await getServiceContainer(appName);
|
|
||||||
const mysqlDumpCommand = `docker exec ${containerId} sh -c "mysqldump --default-character-set=utf8mb4 -u 'root' --password='${databaseRootPassword}' --single-transaction --no-tablespaces --quick '${database}' | gzip"`;
|
|
||||||
|
|
||||||
await execAsync(`${mysqlDumpCommand} | ${rcloneCommand}`);
|
|
||||||
}
|
|
||||||
await sendDatabaseBackupNotifications({
|
|
||||||
applicationName: name,
|
|
||||||
projectName: project.name,
|
|
||||||
databaseType: "mysql",
|
|
||||||
type: "success",
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
await sendDatabaseBackupNotifications({
|
|
||||||
applicationName: name,
|
|
||||||
projectName: project.name,
|
|
||||||
databaseType: "mysql",
|
|
||||||
type: "error",
|
|
||||||
// @ts-ignore
|
|
||||||
errorMessage: error?.message || "Error message not provided",
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import path from "node:path";
|
|
||||||
import type { BackupSchedule } from "@/server/api/services/backup";
|
|
||||||
import type { Postgres } from "@/server/api/services/postgres";
|
|
||||||
import { findProjectById } from "@/server/api/services/project";
|
|
||||||
import {
|
|
||||||
getRemoteServiceContainer,
|
|
||||||
getServiceContainer,
|
|
||||||
} from "../docker/utils";
|
|
||||||
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
|
||||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
|
||||||
import { getS3Credentials } from "./utils";
|
|
||||||
|
|
||||||
export const runPostgresBackup = async (
|
|
||||||
postgres: Postgres,
|
|
||||||
backup: BackupSchedule,
|
|
||||||
) => {
|
|
||||||
const { appName, databaseUser, name, projectId } = postgres;
|
|
||||||
const project = await findProjectById(projectId);
|
|
||||||
|
|
||||||
const { prefix, database } = backup;
|
|
||||||
const destination = backup.destination;
|
|
||||||
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
|
||||||
const bucketDestination = path.join(prefix, backupFileName);
|
|
||||||
try {
|
|
||||||
const rcloneFlags = getS3Credentials(destination);
|
|
||||||
const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`;
|
|
||||||
|
|
||||||
const rcloneCommand = `rclone rcat ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
|
|
||||||
if (postgres.serverId) {
|
|
||||||
const { Id: containerId } = await getRemoteServiceContainer(
|
|
||||||
postgres.serverId,
|
|
||||||
appName,
|
|
||||||
);
|
|
||||||
const pgDumpCommand = `docker exec ${containerId} sh -c "pg_dump -Fc --no-acl --no-owner -h localhost -U ${databaseUser} --no-password '${database}' | gzip"`;
|
|
||||||
|
|
||||||
await execAsyncRemote(
|
|
||||||
postgres.serverId,
|
|
||||||
`${pgDumpCommand} | ${rcloneCommand}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const { Id: containerId } = await getServiceContainer(appName);
|
|
||||||
|
|
||||||
const pgDumpCommand = `docker exec ${containerId} sh -c "pg_dump -Fc --no-acl --no-owner -h localhost -U ${databaseUser} --no-password '${database}' | gzip"`;
|
|
||||||
await execAsync(`${pgDumpCommand} | ${rcloneCommand}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await sendDatabaseBackupNotifications({
|
|
||||||
applicationName: name,
|
|
||||||
projectName: project.name,
|
|
||||||
databaseType: "postgres",
|
|
||||||
type: "success",
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
await sendDatabaseBackupNotifications({
|
|
||||||
applicationName: name,
|
|
||||||
projectName: project.name,
|
|
||||||
databaseType: "postgres",
|
|
||||||
type: "error",
|
|
||||||
// @ts-ignore
|
|
||||||
errorMessage: error?.message || "Error message not provided",
|
|
||||||
});
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Restore
|
|
||||||
// /Applications/pgAdmin 4.app/Contents/SharedSupport/pg_restore --host "localhost" --port "5432" --username "mauricio" --no-password --dbname "postgres" --verbose "/Users/mauricio/Downloads/_databases_2024-04-12T07_02_05.234Z.sql"
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import type { BackupSchedule } from "@/server/api/services/backup";
|
|
||||||
import type { Destination } from "@/server/api/services/destination";
|
|
||||||
import { scheduleJob, scheduledJobs } from "node-schedule";
|
|
||||||
import { runMariadbBackup } from "./mariadb";
|
|
||||||
import { runMongoBackup } from "./mongo";
|
|
||||||
import { runMySqlBackup } from "./mysql";
|
|
||||||
import { runPostgresBackup } from "./postgres";
|
|
||||||
|
|
||||||
export const scheduleBackup = (backup: BackupSchedule) => {
|
|
||||||
const { schedule, backupId, databaseType, postgres, mysql, mongo, mariadb } =
|
|
||||||
backup;
|
|
||||||
scheduleJob(backupId, schedule, async () => {
|
|
||||||
if (databaseType === "postgres" && postgres) {
|
|
||||||
await runPostgresBackup(postgres, backup);
|
|
||||||
} else if (databaseType === "mysql" && mysql) {
|
|
||||||
await runMySqlBackup(mysql, backup);
|
|
||||||
} else if (databaseType === "mongo" && mongo) {
|
|
||||||
await runMongoBackup(mongo, backup);
|
|
||||||
} else if (databaseType === "mariadb" && mariadb) {
|
|
||||||
await runMariadbBackup(mariadb, backup);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const removeScheduleBackup = (backupId: string) => {
|
|
||||||
const currentJob = scheduledJobs[backupId];
|
|
||||||
currentJob?.cancel();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getS3Credentials = (destination: Destination) => {
|
|
||||||
const { accessKey, secretAccessKey, bucket, region, endpoint } = destination;
|
|
||||||
const rcloneFlags = [
|
|
||||||
// `--s3-provider=Cloudflare`,
|
|
||||||
`--s3-access-key-id=${accessKey}`,
|
|
||||||
`--s3-secret-access-key=${secretAccessKey}`,
|
|
||||||
`--s3-region=${region}`,
|
|
||||||
`--s3-endpoint=${endpoint}`,
|
|
||||||
"--s3-no-check-bucket",
|
|
||||||
"--s3-force-path-style",
|
|
||||||
];
|
|
||||||
|
|
||||||
return rcloneFlags;
|
|
||||||
};
|
|
||||||
@@ -1,216 +0,0 @@
|
|||||||
import {
|
|
||||||
createWriteStream,
|
|
||||||
existsSync,
|
|
||||||
mkdirSync,
|
|
||||||
writeFileSync,
|
|
||||||
} from "node:fs";
|
|
||||||
import { dirname, join } from "node:path";
|
|
||||||
import { paths } from "@/server/constants";
|
|
||||||
import type { InferResultType } from "@/server/types/with";
|
|
||||||
import boxen from "boxen";
|
|
||||||
import {
|
|
||||||
writeDomainsToCompose,
|
|
||||||
writeDomainsToComposeRemote,
|
|
||||||
} from "../docker/domain";
|
|
||||||
import { encodeBase64, prepareEnvironmentVariables } from "../docker/utils";
|
|
||||||
import { execAsyncRemote } from "../process/execAsync";
|
|
||||||
import { spawnAsync } from "../process/spawnAsync";
|
|
||||||
|
|
||||||
export type ComposeNested = InferResultType<
|
|
||||||
"compose",
|
|
||||||
{ project: true; mounts: true; domains: true }
|
|
||||||
>;
|
|
||||||
export const buildCompose = async (compose: ComposeNested, logPath: string) => {
|
|
||||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
|
||||||
const { sourceType, appName, mounts, composeType, domains } = compose;
|
|
||||||
try {
|
|
||||||
const { COMPOSE_PATH } = paths();
|
|
||||||
const command = createCommand(compose);
|
|
||||||
await writeDomainsToCompose(compose, domains);
|
|
||||||
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, "code");
|
|
||||||
await spawnAsync(
|
|
||||||
"docker",
|
|
||||||
[...command.split(" ")],
|
|
||||||
(data) => {
|
|
||||||
if (writeStream.writable) {
|
|
||||||
writeStream.write(data.toString());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
cwd: projectPath,
|
|
||||||
env: {
|
|
||||||
NODE_ENV: process.env.NODE_ENV,
|
|
||||||
PATH: process.env.PATH,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
writeStream.write("Docker Compose Deployed: ✅");
|
|
||||||
} catch (error) {
|
|
||||||
writeStream.write("Error ❌");
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
writeStream.end();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getBuildComposeCommand = async (
|
|
||||||
compose: ComposeNested,
|
|
||||||
logPath: string,
|
|
||||||
) => {
|
|
||||||
const { COMPOSE_PATH } = paths(true);
|
|
||||||
const { sourceType, appName, mounts, composeType, domains, composePath } =
|
|
||||||
compose;
|
|
||||||
const command = createCommand(compose);
|
|
||||||
const envCommand = getCreateEnvFileCommand(compose);
|
|
||||||
const projectPath = join(COMPOSE_PATH, compose.appName, "code");
|
|
||||||
|
|
||||||
const newCompose = await writeDomainsToComposeRemote(
|
|
||||||
compose,
|
|
||||||
domains,
|
|
||||||
logPath,
|
|
||||||
);
|
|
||||||
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",
|
|
||||||
});
|
|
||||||
|
|
||||||
const bashCommand = `
|
|
||||||
set -e
|
|
||||||
{
|
|
||||||
echo "${logBox}" >> "${logPath}"
|
|
||||||
|
|
||||||
${newCompose}
|
|
||||||
|
|
||||||
${envCommand}
|
|
||||||
|
|
||||||
cd "${projectPath}";
|
|
||||||
|
|
||||||
docker ${command.split(" ").join(" ")} >> "${logPath}" 2>&1 || { echo "Error: ❌ Docker command failed" >> "${logPath}"; exit 1; }
|
|
||||||
|
|
||||||
echo "Docker Compose Deployed: ✅" >> "${logPath}"
|
|
||||||
} || {
|
|
||||||
echo "Error: ❌ Script execution failed" >> "${logPath}"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
return await execAsyncRemote(compose.serverId, bashCommand);
|
|
||||||
};
|
|
||||||
|
|
||||||
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 { COMPOSE_PATH } = paths();
|
|
||||||
const { env, composePath, appName } = compose;
|
|
||||||
const composeFilePath =
|
|
||||||
join(COMPOSE_PATH, appName, "code", composePath) ||
|
|
||||||
join(COMPOSE_PATH, appName, "code", "docker-compose.yml");
|
|
||||||
|
|
||||||
const envFilePath = join(dirname(composeFilePath), ".env");
|
|
||||||
let envContent = env || "";
|
|
||||||
if (!envContent.includes("DOCKER_CONFIG")) {
|
|
||||||
envContent += "\nDOCKER_CONFIG=/root/.docker/config.json";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (compose.randomize) {
|
|
||||||
envContent += `\nCOMPOSE_PREFIX=${compose.suffix}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const envFileContent = prepareEnvironmentVariables(envContent).join("\n");
|
|
||||||
|
|
||||||
if (!existsSync(dirname(envFilePath))) {
|
|
||||||
mkdirSync(dirname(envFilePath), { recursive: true });
|
|
||||||
}
|
|
||||||
writeFileSync(envFilePath, envFileContent);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getCreateEnvFileCommand = (compose: ComposeNested) => {
|
|
||||||
const { COMPOSE_PATH } = paths(true);
|
|
||||||
const { env, composePath, appName } = compose;
|
|
||||||
const composeFilePath =
|
|
||||||
join(COMPOSE_PATH, appName, "code", composePath) ||
|
|
||||||
join(COMPOSE_PATH, appName, "code", "docker-compose.yml");
|
|
||||||
|
|
||||||
const envFilePath = join(dirname(composeFilePath), ".env");
|
|
||||||
|
|
||||||
let envContent = env || "";
|
|
||||||
if (!envContent.includes("DOCKER_CONFIG")) {
|
|
||||||
envContent += "\nDOCKER_CONFIG=/root/.docker/config.json";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (compose.randomize) {
|
|
||||||
envContent += `\nCOMPOSE_PREFIX=${compose.suffix}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const envFileContent = prepareEnvironmentVariables(envContent).join("\n");
|
|
||||||
|
|
||||||
const encodedContent = encodeBase64(envFileContent);
|
|
||||||
return `
|
|
||||||
touch ${envFilePath};
|
|
||||||
echo "${encodedContent}" | base64 -d > "${envFilePath}";
|
|
||||||
`;
|
|
||||||
};
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
import type { WriteStream } from "node:fs";
|
|
||||||
import { prepareEnvironmentVariables } from "@/server/utils/docker/utils";
|
|
||||||
import type { ApplicationNested } from ".";
|
|
||||||
import {
|
|
||||||
getBuildAppDirectory,
|
|
||||||
getDockerContextPath,
|
|
||||||
} from "../filesystem/directory";
|
|
||||||
import { spawnAsync } from "../process/spawnAsync";
|
|
||||||
import { createEnvFile, createEnvFileCommand } from "./utils";
|
|
||||||
|
|
||||||
export const buildCustomDocker = async (
|
|
||||||
application: ApplicationNested,
|
|
||||||
writeStream: WriteStream,
|
|
||||||
) => {
|
|
||||||
const { appName, env, publishDirectory, buildArgs, dockerBuildStage } =
|
|
||||||
application;
|
|
||||||
const dockerFilePath = getBuildAppDirectory(application);
|
|
||||||
try {
|
|
||||||
const image = `${appName}`;
|
|
||||||
|
|
||||||
const defaultContextPath =
|
|
||||||
dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || ".";
|
|
||||||
const args = prepareEnvironmentVariables(buildArgs);
|
|
||||||
|
|
||||||
const dockerContextPath = getDockerContextPath(application);
|
|
||||||
|
|
||||||
const commandArgs = ["build", "-t", image, "-f", dockerFilePath, "."];
|
|
||||||
|
|
||||||
if (dockerBuildStage) {
|
|
||||||
commandArgs.push("--target", dockerBuildStage);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const arg of args) {
|
|
||||||
commandArgs.push("--build-arg", arg);
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
Do not generate an environment file when publishDirectory is specified,
|
|
||||||
as it could be publicly exposed.
|
|
||||||
*/
|
|
||||||
if (!publishDirectory) {
|
|
||||||
createEnvFile(dockerFilePath, env);
|
|
||||||
}
|
|
||||||
|
|
||||||
await spawnAsync(
|
|
||||||
"docker",
|
|
||||||
commandArgs,
|
|
||||||
(data) => {
|
|
||||||
if (writeStream.writable) {
|
|
||||||
writeStream.write(data);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
cwd: dockerContextPath || defaultContextPath,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getDockerCommand = (
|
|
||||||
application: ApplicationNested,
|
|
||||||
logPath: string,
|
|
||||||
) => {
|
|
||||||
const { appName, env, publishDirectory, buildArgs, dockerBuildStage } =
|
|
||||||
application;
|
|
||||||
const dockerFilePath = getBuildAppDirectory(application);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const image = `${appName}`;
|
|
||||||
|
|
||||||
const defaultContextPath =
|
|
||||||
dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || ".";
|
|
||||||
const args = prepareEnvironmentVariables(buildArgs);
|
|
||||||
|
|
||||||
const dockerContextPath =
|
|
||||||
getDockerContextPath(application) || defaultContextPath;
|
|
||||||
|
|
||||||
const commandArgs = ["build", "-t", image, "-f", dockerFilePath, "."];
|
|
||||||
|
|
||||||
if (dockerBuildStage) {
|
|
||||||
commandArgs.push("--target", dockerBuildStage);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const arg of args) {
|
|
||||||
commandArgs.push("--build-arg", arg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Do not generate an environment file when publishDirectory is specified,
|
|
||||||
as it could be publicly exposed.
|
|
||||||
*/
|
|
||||||
let command = "";
|
|
||||||
if (!publishDirectory) {
|
|
||||||
command += createEnvFileCommand(dockerFilePath, env);
|
|
||||||
}
|
|
||||||
|
|
||||||
command = `
|
|
||||||
echo "Building ${appName}" >> ${logPath};
|
|
||||||
cd ${dockerContextPath} >> ${logPath} 2>> ${logPath} || {
|
|
||||||
echo "❌ The path ${dockerContextPath} does not exist" >> ${logPath};
|
|
||||||
exit 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
docker ${commandArgs.join(" ")} >> ${logPath} 2>> ${logPath} || {
|
|
||||||
echo "❌ Docker build failed" >> ${logPath};
|
|
||||||
exit 1;
|
|
||||||
}
|
|
||||||
echo "✅ Docker build completed." >> ${logPath};
|
|
||||||
`;
|
|
||||||
|
|
||||||
return command;
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
import fs from "node:fs/promises";
|
|
||||||
import path, { join } from "node:path";
|
|
||||||
import type { Application } from "@/server/api/services/application";
|
|
||||||
import { findServerById } from "@/server/api/services/server";
|
|
||||||
import { paths } from "@/server/constants";
|
|
||||||
import AdmZip from "adm-zip";
|
|
||||||
import { Client, type SFTPWrapper } from "ssh2";
|
|
||||||
import {
|
|
||||||
recreateDirectory,
|
|
||||||
recreateDirectoryRemote,
|
|
||||||
} from "../filesystem/directory";
|
|
||||||
import { readSSHKey } from "../filesystem/ssh";
|
|
||||||
import { execAsyncRemote } from "../process/execAsync";
|
|
||||||
|
|
||||||
export const unzipDrop = async (zipFile: File, application: Application) => {
|
|
||||||
let sftp: SFTPWrapper | null = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { appName } = application;
|
|
||||||
const { APPLICATIONS_PATH } = paths(!!application.serverId);
|
|
||||||
const outputPath = join(APPLICATIONS_PATH, appName, "code");
|
|
||||||
if (application.serverId) {
|
|
||||||
await recreateDirectoryRemote(outputPath, application.serverId);
|
|
||||||
} else {
|
|
||||||
await recreateDirectory(outputPath);
|
|
||||||
}
|
|
||||||
const arrayBuffer = await zipFile.arrayBuffer();
|
|
||||||
const buffer = Buffer.from(arrayBuffer);
|
|
||||||
|
|
||||||
const zip = new AdmZip(buffer);
|
|
||||||
const zipEntries = zip.getEntries();
|
|
||||||
|
|
||||||
const rootEntries = zipEntries.filter(
|
|
||||||
(entry) =>
|
|
||||||
entry.entryName.split("/").length === 1 ||
|
|
||||||
(entry.entryName.split("/").length === 2 &&
|
|
||||||
entry.entryName.endsWith("/")),
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasSingleRootFolder = !!(
|
|
||||||
rootEntries.length === 1 && rootEntries[0]?.isDirectory
|
|
||||||
);
|
|
||||||
const rootFolderName = hasSingleRootFolder
|
|
||||||
? rootEntries[0]?.entryName.split("/")[0]
|
|
||||||
: "";
|
|
||||||
|
|
||||||
if (application.serverId) {
|
|
||||||
sftp = await getSFTPConnection(application.serverId);
|
|
||||||
}
|
|
||||||
for (const entry of zipEntries) {
|
|
||||||
let filePath = entry.entryName;
|
|
||||||
|
|
||||||
if (
|
|
||||||
hasSingleRootFolder &&
|
|
||||||
rootFolderName &&
|
|
||||||
filePath.startsWith(`${rootFolderName}/`)
|
|
||||||
) {
|
|
||||||
filePath = filePath.slice(rootFolderName?.length + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!filePath) continue;
|
|
||||||
|
|
||||||
const fullPath = path.join(outputPath, filePath);
|
|
||||||
|
|
||||||
if (application.serverId) {
|
|
||||||
if (entry.isDirectory) {
|
|
||||||
await execAsyncRemote(application.serverId, `mkdir -p ${fullPath}`);
|
|
||||||
} else {
|
|
||||||
if (sftp === null) throw new Error("No SFTP connection available");
|
|
||||||
await uploadFileToServer(sftp, entry.getData(), fullPath);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (entry.isDirectory) {
|
|
||||||
await fs.mkdir(fullPath, { recursive: true });
|
|
||||||
} else {
|
|
||||||
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
||||||
await fs.writeFile(fullPath, entry.getData());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error processing ZIP file:", error);
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
sftp?.end();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSFTPConnection = async (serverId: string): Promise<SFTPWrapper> => {
|
|
||||||
const server = await findServerById(serverId);
|
|
||||||
if (!server.sshKeyId) throw new Error("No SSH key available for this server");
|
|
||||||
|
|
||||||
const keys = await readSSHKey(server.sshKeyId);
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const conn = new Client();
|
|
||||||
conn
|
|
||||||
.on("ready", () => {
|
|
||||||
conn.sftp((err, sftp) => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
resolve(sftp);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.connect({
|
|
||||||
host: server.ipAddress,
|
|
||||||
port: server.port,
|
|
||||||
username: server.username,
|
|
||||||
privateKey: keys.privateKey,
|
|
||||||
timeout: 99999,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const uploadFileToServer = (
|
|
||||||
sftp: SFTPWrapper,
|
|
||||||
data: Buffer,
|
|
||||||
remotePath: string,
|
|
||||||
): Promise<void> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
sftp.writeFile(remotePath, data, (err) => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
import type { WriteStream } from "node:fs";
|
|
||||||
import type { ApplicationNested } from ".";
|
|
||||||
import { prepareEnvironmentVariables } from "../docker/utils";
|
|
||||||
import { getBuildAppDirectory } from "../filesystem/directory";
|
|
||||||
import { spawnAsync } from "../process/spawnAsync";
|
|
||||||
|
|
||||||
// TODO: integrate in the vps sudo chown -R $(whoami) ~/.docker
|
|
||||||
export const buildHeroku = async (
|
|
||||||
application: ApplicationNested,
|
|
||||||
writeStream: WriteStream,
|
|
||||||
) => {
|
|
||||||
const { env, appName } = application;
|
|
||||||
const buildAppDirectory = getBuildAppDirectory(application);
|
|
||||||
const envVariables = prepareEnvironmentVariables(env);
|
|
||||||
try {
|
|
||||||
const args = [
|
|
||||||
"build",
|
|
||||||
appName,
|
|
||||||
"--path",
|
|
||||||
buildAppDirectory,
|
|
||||||
"--builder",
|
|
||||||
"heroku/builder:24",
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const env of envVariables) {
|
|
||||||
args.push("--env", env);
|
|
||||||
}
|
|
||||||
|
|
||||||
await spawnAsync("pack", args, (data) => {
|
|
||||||
if (writeStream.writable) {
|
|
||||||
writeStream.write(data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getHerokuCommand = (
|
|
||||||
application: ApplicationNested,
|
|
||||||
logPath: string,
|
|
||||||
) => {
|
|
||||||
const { env, appName } = application;
|
|
||||||
|
|
||||||
const buildAppDirectory = getBuildAppDirectory(application);
|
|
||||||
const envVariables = prepareEnvironmentVariables(env);
|
|
||||||
|
|
||||||
const args = [
|
|
||||||
"build",
|
|
||||||
appName,
|
|
||||||
"--path",
|
|
||||||
buildAppDirectory,
|
|
||||||
"--builder",
|
|
||||||
"heroku/builder:24",
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const env of envVariables) {
|
|
||||||
args.push("--env", env);
|
|
||||||
}
|
|
||||||
|
|
||||||
const command = `pack ${args.join(" ")}`;
|
|
||||||
const bashCommand = `
|
|
||||||
echo "Starting heroku build..." >> ${logPath};
|
|
||||||
${command} >> ${logPath} 2>> ${logPath} || {
|
|
||||||
echo "❌ Heroku build failed" >> ${logPath};
|
|
||||||
exit 1;
|
|
||||||
}
|
|
||||||
echo "✅ Heroku build completed." >> ${logPath};
|
|
||||||
`;
|
|
||||||
|
|
||||||
return bashCommand;
|
|
||||||
};
|
|
||||||
@@ -1,216 +0,0 @@
|
|||||||
import { createWriteStream } from "node:fs";
|
|
||||||
import type { InferResultType } from "@/server/types/with";
|
|
||||||
import type { CreateServiceOptions } from "dockerode";
|
|
||||||
import { uploadImage } from "../cluster/upload";
|
|
||||||
import {
|
|
||||||
calculateResources,
|
|
||||||
generateBindMounts,
|
|
||||||
generateConfigContainer,
|
|
||||||
generateFileMounts,
|
|
||||||
generateVolumeMounts,
|
|
||||||
prepareEnvironmentVariables,
|
|
||||||
} from "../docker/utils";
|
|
||||||
import { getRemoteDocker } from "../servers/remote-docker";
|
|
||||||
import { buildCustomDocker, getDockerCommand } from "./docker-file";
|
|
||||||
import { buildHeroku, getHerokuCommand } from "./heroku";
|
|
||||||
import { buildNixpacks, getNixpacksCommand } from "./nixpacks";
|
|
||||||
import { buildPaketo, getPaketoCommand } from "./paketo";
|
|
||||||
import { buildStatic, getStaticCommand } from "./static";
|
|
||||||
|
|
||||||
// NIXPACKS codeDirectory = where is the path of the code directory
|
|
||||||
// HEROKU codeDirectory = where is the path of the code directory
|
|
||||||
// PAKETO codeDirectory = where is the path of the code directory
|
|
||||||
// DOCKERFILE codeDirectory = where is the exact path of the (Dockerfile)
|
|
||||||
export type ApplicationNested = InferResultType<
|
|
||||||
"applications",
|
|
||||||
{ mounts: true; security: true; redirects: true; ports: true; registry: true }
|
|
||||||
>;
|
|
||||||
export const buildApplication = async (
|
|
||||||
application: ApplicationNested,
|
|
||||||
logPath: string,
|
|
||||||
) => {
|
|
||||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
|
||||||
const { buildType, sourceType } = application;
|
|
||||||
try {
|
|
||||||
writeStream.write(
|
|
||||||
`\nBuild ${buildType}: ✅\nSource Type: ${sourceType}: ✅\n`,
|
|
||||||
);
|
|
||||||
console.log(`Build ${buildType}: ✅`);
|
|
||||||
if (buildType === "nixpacks") {
|
|
||||||
await buildNixpacks(application, writeStream);
|
|
||||||
} else if (buildType === "heroku_buildpacks") {
|
|
||||||
await buildHeroku(application, writeStream);
|
|
||||||
} else if (buildType === "paketo_buildpacks") {
|
|
||||||
await buildPaketo(application, writeStream);
|
|
||||||
} else if (buildType === "dockerfile") {
|
|
||||||
await buildCustomDocker(application, writeStream);
|
|
||||||
} else if (buildType === "static") {
|
|
||||||
await buildStatic(application, writeStream);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (application.registryId) {
|
|
||||||
await uploadImage(application, writeStream);
|
|
||||||
}
|
|
||||||
await mechanizeDockerContainer(application);
|
|
||||||
writeStream.write("Docker Deployed: ✅");
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
writeStream.write(`Error ❌\n${error?.message}`);
|
|
||||||
} else {
|
|
||||||
writeStream.write("Error ❌");
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
writeStream.end();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getBuildCommand = (
|
|
||||||
application: ApplicationNested,
|
|
||||||
logPath: string,
|
|
||||||
) => {
|
|
||||||
const { buildType } = application;
|
|
||||||
switch (buildType) {
|
|
||||||
case "nixpacks":
|
|
||||||
return getNixpacksCommand(application, logPath);
|
|
||||||
case "heroku_buildpacks":
|
|
||||||
return getHerokuCommand(application, logPath);
|
|
||||||
case "paketo_buildpacks":
|
|
||||||
return getPaketoCommand(application, logPath);
|
|
||||||
case "static":
|
|
||||||
return getStaticCommand(application, logPath);
|
|
||||||
case "dockerfile":
|
|
||||||
return getDockerCommand(application, logPath);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const mechanizeDockerContainer = async (
|
|
||||||
application: ApplicationNested,
|
|
||||||
) => {
|
|
||||||
const {
|
|
||||||
appName,
|
|
||||||
env,
|
|
||||||
mounts,
|
|
||||||
cpuLimit,
|
|
||||||
memoryLimit,
|
|
||||||
memoryReservation,
|
|
||||||
cpuReservation,
|
|
||||||
command,
|
|
||||||
ports,
|
|
||||||
} = application;
|
|
||||||
|
|
||||||
const resources = calculateResources({
|
|
||||||
memoryLimit,
|
|
||||||
memoryReservation,
|
|
||||||
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, application);
|
|
||||||
const envVariables = prepareEnvironmentVariables(env);
|
|
||||||
|
|
||||||
const image = getImageName(application);
|
|
||||||
const authConfig = getAuthConfig(application);
|
|
||||||
const docker = await getRemoteDocker(application.serverId);
|
|
||||||
|
|
||||||
const settings: CreateServiceOptions = {
|
|
||||||
authconfig: authConfig,
|
|
||||||
Name: appName,
|
|
||||||
TaskTemplate: {
|
|
||||||
ContainerSpec: {
|
|
||||||
HealthCheck,
|
|
||||||
Image: image,
|
|
||||||
Env: envVariables,
|
|
||||||
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
|
|
||||||
...(command
|
|
||||||
? {
|
|
||||||
Command: ["/bin/sh"],
|
|
||||||
Args: ["-c", command],
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
Labels,
|
|
||||||
},
|
|
||||||
Networks,
|
|
||||||
RestartPolicy,
|
|
||||||
Placement,
|
|
||||||
Resources: {
|
|
||||||
...resources,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Mode,
|
|
||||||
RollbackConfig,
|
|
||||||
EndpointSpec: {
|
|
||||||
Ports: ports.map((port) => ({
|
|
||||||
Protocol: port.protocol,
|
|
||||||
TargetPort: port.targetPort,
|
|
||||||
PublishedPort: port.publishedPort,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
UpdateConfig,
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const service = docker.getService(appName);
|
|
||||||
const inspect = await service.inspect();
|
|
||||||
await service.update({
|
|
||||||
version: Number.parseInt(inspect.Version.Index),
|
|
||||||
...settings,
|
|
||||||
TaskTemplate: {
|
|
||||||
...settings.TaskTemplate,
|
|
||||||
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
await docker.createService(settings);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
import type { WriteStream } from "node:fs";
|
|
||||||
import path from "node:path";
|
|
||||||
import { buildStatic, getStaticCommand } from "@/server/utils/builders/static";
|
|
||||||
import { nanoid } from "nanoid";
|
|
||||||
import type { ApplicationNested } from ".";
|
|
||||||
import { prepareEnvironmentVariables } from "../docker/utils";
|
|
||||||
import { getBuildAppDirectory } from "../filesystem/directory";
|
|
||||||
import { spawnAsync } from "../process/spawnAsync";
|
|
||||||
|
|
||||||
export const buildNixpacks = async (
|
|
||||||
application: ApplicationNested,
|
|
||||||
writeStream: WriteStream,
|
|
||||||
) => {
|
|
||||||
const { env, appName, publishDirectory, serverId } = application;
|
|
||||||
|
|
||||||
const buildAppDirectory = getBuildAppDirectory(application);
|
|
||||||
const buildContainerId = `${appName}-${nanoid(10)}`;
|
|
||||||
const envVariables = prepareEnvironmentVariables(env);
|
|
||||||
|
|
||||||
const writeToStream = (data: string) => {
|
|
||||||
if (writeStream.writable) {
|
|
||||||
writeStream.write(data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const args = ["build", buildAppDirectory, "--name", appName];
|
|
||||||
|
|
||||||
for (const env of envVariables) {
|
|
||||||
args.push("--env", env);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (publishDirectory) {
|
|
||||||
/* No need for any start command, since we'll use nginx later on */
|
|
||||||
args.push("--no-error-without-start");
|
|
||||||
}
|
|
||||||
|
|
||||||
await spawnAsync("nixpacks", args, writeToStream);
|
|
||||||
|
|
||||||
/*
|
|
||||||
Run the container with the image created by nixpacks,
|
|
||||||
and copy the artifacts on the host filesystem.
|
|
||||||
Then, remove the container and create a static build.
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (publishDirectory) {
|
|
||||||
await spawnAsync(
|
|
||||||
"docker",
|
|
||||||
["create", "--name", buildContainerId, appName],
|
|
||||||
writeToStream,
|
|
||||||
);
|
|
||||||
|
|
||||||
await spawnAsync(
|
|
||||||
"docker",
|
|
||||||
[
|
|
||||||
"cp",
|
|
||||||
`${buildContainerId}:/app/${publishDirectory}`,
|
|
||||||
path.join(buildAppDirectory, publishDirectory),
|
|
||||||
],
|
|
||||||
writeToStream,
|
|
||||||
);
|
|
||||||
|
|
||||||
await spawnAsync("docker", ["rm", buildContainerId], writeToStream);
|
|
||||||
|
|
||||||
await buildStatic(application, writeStream);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
await spawnAsync("docker", ["rm", buildContainerId], writeToStream);
|
|
||||||
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getNixpacksCommand = (
|
|
||||||
application: ApplicationNested,
|
|
||||||
logPath: string,
|
|
||||||
) => {
|
|
||||||
const { env, appName, publishDirectory, serverId } = application;
|
|
||||||
|
|
||||||
const buildAppDirectory = getBuildAppDirectory(application);
|
|
||||||
const buildContainerId = `${appName}-${nanoid(10)}`;
|
|
||||||
const envVariables = prepareEnvironmentVariables(env);
|
|
||||||
|
|
||||||
const args = ["build", buildAppDirectory, "--name", appName];
|
|
||||||
|
|
||||||
for (const env of envVariables) {
|
|
||||||
args.push("--env", env);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (publishDirectory) {
|
|
||||||
/* No need for any start command, since we'll use nginx later on */
|
|
||||||
args.push("--no-error-without-start");
|
|
||||||
}
|
|
||||||
const command = `nixpacks ${args.join(" ")}`;
|
|
||||||
let bashCommand = `
|
|
||||||
echo "Starting nixpacks build..." >> ${logPath};
|
|
||||||
${command} >> ${logPath} 2>> ${logPath} || {
|
|
||||||
echo "❌ Nixpacks build failed" >> ${logPath};
|
|
||||||
exit 1;
|
|
||||||
}
|
|
||||||
echo "✅ Nixpacks build completed." >> ${logPath};
|
|
||||||
`;
|
|
||||||
|
|
||||||
/*
|
|
||||||
Run the container with the image created by nixpacks,
|
|
||||||
and copy the artifacts on the host filesystem.
|
|
||||||
Then, remove the container and create a static build.
|
|
||||||
*/
|
|
||||||
if (publishDirectory) {
|
|
||||||
bashCommand += `
|
|
||||||
docker create --name ${buildContainerId} ${appName}
|
|
||||||
docker cp ${buildContainerId}:/app/${publishDirectory} ${path.join(buildAppDirectory, publishDirectory)} >> ${logPath} 2>> ${logPath} || {
|
|
||||||
docker rm ${buildContainerId}
|
|
||||||
echo "❌ Copying ${publishDirectory} to ${path.join(buildAppDirectory, publishDirectory)} failed" >> ${logPath};
|
|
||||||
exit 1;
|
|
||||||
}
|
|
||||||
docker rm ${buildContainerId}
|
|
||||||
${getStaticCommand(application, logPath)}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return bashCommand;
|
|
||||||
};
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import type { WriteStream } from "node:fs";
|
|
||||||
import type { ApplicationNested } from ".";
|
|
||||||
import { prepareEnvironmentVariables } from "../docker/utils";
|
|
||||||
import { getBuildAppDirectory } from "../filesystem/directory";
|
|
||||||
import { spawnAsync } from "../process/spawnAsync";
|
|
||||||
|
|
||||||
export const buildPaketo = async (
|
|
||||||
application: ApplicationNested,
|
|
||||||
writeStream: WriteStream,
|
|
||||||
) => {
|
|
||||||
const { env, appName } = application;
|
|
||||||
const buildAppDirectory = getBuildAppDirectory(application);
|
|
||||||
const envVariables = prepareEnvironmentVariables(env);
|
|
||||||
try {
|
|
||||||
const args = [
|
|
||||||
"build",
|
|
||||||
appName,
|
|
||||||
"--path",
|
|
||||||
buildAppDirectory,
|
|
||||||
"--builder",
|
|
||||||
"paketobuildpacks/builder-jammy-full",
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const env of envVariables) {
|
|
||||||
args.push("--env", env);
|
|
||||||
}
|
|
||||||
|
|
||||||
await spawnAsync("pack", args, (data) => {
|
|
||||||
if (writeStream.writable) {
|
|
||||||
writeStream.write(data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getPaketoCommand = (
|
|
||||||
application: ApplicationNested,
|
|
||||||
logPath: string,
|
|
||||||
) => {
|
|
||||||
const { env, appName } = application;
|
|
||||||
|
|
||||||
const buildAppDirectory = getBuildAppDirectory(application);
|
|
||||||
const envVariables = prepareEnvironmentVariables(env);
|
|
||||||
|
|
||||||
const args = [
|
|
||||||
"build",
|
|
||||||
appName,
|
|
||||||
"--path",
|
|
||||||
buildAppDirectory,
|
|
||||||
"--builder",
|
|
||||||
"paketobuildpacks/builder-jammy-full",
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const env of envVariables) {
|
|
||||||
args.push("--env", env);
|
|
||||||
}
|
|
||||||
|
|
||||||
const command = `pack ${args.join(" ")}`;
|
|
||||||
const bashCommand = `
|
|
||||||
echo "Starting Paketo build..." >> ${logPath};
|
|
||||||
${command} >> ${logPath} 2>> ${logPath} || {
|
|
||||||
echo "❌ Paketo build failed" >> ${logPath};
|
|
||||||
exit 1;
|
|
||||||
}
|
|
||||||
echo "✅ Paketo build completed." >> ${logPath};
|
|
||||||
`;
|
|
||||||
|
|
||||||
return bashCommand;
|
|
||||||
};
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import type { WriteStream } from "node:fs";
|
|
||||||
import {
|
|
||||||
buildCustomDocker,
|
|
||||||
getDockerCommand,
|
|
||||||
} from "@/server/utils/builders/docker-file";
|
|
||||||
import type { ApplicationNested } from ".";
|
|
||||||
import { createFile, getCreateFileCommand } from "../docker/utils";
|
|
||||||
import { getBuildAppDirectory } from "../filesystem/directory";
|
|
||||||
|
|
||||||
export const buildStatic = async (
|
|
||||||
application: ApplicationNested,
|
|
||||||
writeStream: WriteStream,
|
|
||||||
) => {
|
|
||||||
const { publishDirectory } = application;
|
|
||||||
const buildAppDirectory = getBuildAppDirectory(application);
|
|
||||||
|
|
||||||
try {
|
|
||||||
createFile(
|
|
||||||
buildAppDirectory,
|
|
||||||
"Dockerfile",
|
|
||||||
[
|
|
||||||
"FROM nginx:alpine",
|
|
||||||
"WORKDIR /usr/share/nginx/html/",
|
|
||||||
`COPY ${publishDirectory || "."} .`,
|
|
||||||
].join("\n"),
|
|
||||||
);
|
|
||||||
|
|
||||||
await buildCustomDocker(
|
|
||||||
{
|
|
||||||
...application,
|
|
||||||
buildType: "dockerfile",
|
|
||||||
dockerfile: "Dockerfile",
|
|
||||||
},
|
|
||||||
writeStream,
|
|
||||||
);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getStaticCommand = (
|
|
||||||
application: ApplicationNested,
|
|
||||||
logPath: string,
|
|
||||||
) => {
|
|
||||||
const { publishDirectory } = application;
|
|
||||||
const buildAppDirectory = getBuildAppDirectory(application);
|
|
||||||
|
|
||||||
let command = getCreateFileCommand(
|
|
||||||
buildAppDirectory,
|
|
||||||
"Dockerfile",
|
|
||||||
[
|
|
||||||
"FROM nginx:alpine",
|
|
||||||
"WORKDIR /usr/share/nginx/html/",
|
|
||||||
`COPY ${publishDirectory || "."} .`,
|
|
||||||
].join("\n"),
|
|
||||||
);
|
|
||||||
|
|
||||||
command += getDockerCommand(
|
|
||||||
{
|
|
||||||
...application,
|
|
||||||
buildType: "dockerfile",
|
|
||||||
dockerfile: "Dockerfile",
|
|
||||||
},
|
|
||||||
logPath,
|
|
||||||
);
|
|
||||||
return command;
|
|
||||||
};
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
||||||
import { dirname, join } from "node:path";
|
|
||||||
import { prepareEnvironmentVariables } from "../docker/utils";
|
|
||||||
|
|
||||||
export const createEnvFile = (directory: string, env: string | null) => {
|
|
||||||
const envFilePath = join(dirname(directory), ".env");
|
|
||||||
if (!existsSync(dirname(envFilePath))) {
|
|
||||||
mkdirSync(dirname(envFilePath), { recursive: true });
|
|
||||||
}
|
|
||||||
const envFileContent = prepareEnvironmentVariables(env).join("\n");
|
|
||||||
writeFileSync(envFilePath, envFileContent);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createEnvFileCommand = (directory: string, env: string | null) => {
|
|
||||||
const envFilePath = join(dirname(directory), ".env");
|
|
||||||
if (!existsSync(dirname(envFilePath))) {
|
|
||||||
mkdirSync(dirname(envFilePath), { recursive: true });
|
|
||||||
}
|
|
||||||
const envFileContent = prepareEnvironmentVariables(env).join("\n");
|
|
||||||
return `echo "${envFileContent}" > ${envFilePath}`;
|
|
||||||
};
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
import type { WriteStream } from "node:fs";
|
|
||||||
import type { ApplicationNested } from "../builders";
|
|
||||||
import { spawnAsync } from "../process/spawnAsync";
|
|
||||||
|
|
||||||
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
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
import type { InferResultType } from "@/server/types/with";
|
|
||||||
import type { CreateServiceOptions } from "dockerode";
|
|
||||||
import {
|
|
||||||
calculateResources,
|
|
||||||
generateBindMounts,
|
|
||||||
generateFileMounts,
|
|
||||||
generateVolumeMounts,
|
|
||||||
prepareEnvironmentVariables,
|
|
||||||
} from "../docker/utils";
|
|
||||||
import { getRemoteDocker } from "../servers/remote-docker";
|
|
||||||
|
|
||||||
export type MariadbNested = InferResultType<"mariadb", { mounts: true }>;
|
|
||||||
export const buildMariadb = async (mariadb: MariadbNested) => {
|
|
||||||
const {
|
|
||||||
appName,
|
|
||||||
env,
|
|
||||||
externalPort,
|
|
||||||
dockerImage,
|
|
||||||
memoryLimit,
|
|
||||||
memoryReservation,
|
|
||||||
databaseName,
|
|
||||||
databaseUser,
|
|
||||||
databasePassword,
|
|
||||||
databaseRootPassword,
|
|
||||||
cpuLimit,
|
|
||||||
cpuReservation,
|
|
||||||
command,
|
|
||||||
mounts,
|
|
||||||
} = mariadb;
|
|
||||||
|
|
||||||
const defaultMariadbEnv = `MARIADB_DATABASE=${databaseName}\nMARIADB_USER=${databaseUser}\nMARIADB_PASSWORD=${databasePassword}\nMARIADB_ROOT_PASSWORD=${databaseRootPassword}${
|
|
||||||
env ? `\n${env}` : ""
|
|
||||||
}`;
|
|
||||||
const resources = calculateResources({
|
|
||||||
memoryLimit,
|
|
||||||
memoryReservation,
|
|
||||||
cpuLimit,
|
|
||||||
cpuReservation,
|
|
||||||
});
|
|
||||||
const envVariables = prepareEnvironmentVariables(defaultMariadbEnv);
|
|
||||||
const volumesMount = generateVolumeMounts(mounts);
|
|
||||||
const bindsMount = generateBindMounts(mounts);
|
|
||||||
const filesMount = generateFileMounts(appName, mariadb);
|
|
||||||
|
|
||||||
const docker = await getRemoteDocker(mariadb.serverId);
|
|
||||||
|
|
||||||
const settings: CreateServiceOptions = {
|
|
||||||
Name: appName,
|
|
||||||
TaskTemplate: {
|
|
||||||
ContainerSpec: {
|
|
||||||
Image: dockerImage,
|
|
||||||
Env: envVariables,
|
|
||||||
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
|
|
||||||
...(command
|
|
||||||
? {
|
|
||||||
Command: ["/bin/sh"],
|
|
||||||
Args: ["-c", command],
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
},
|
|
||||||
Networks: [{ Target: "dokploy-network" }],
|
|
||||||
Resources: {
|
|
||||||
...resources,
|
|
||||||
},
|
|
||||||
Placement: {
|
|
||||||
Constraints: ["node.role==manager"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Mode: {
|
|
||||||
Replicated: {
|
|
||||||
Replicas: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
EndpointSpec: {
|
|
||||||
Mode: "dnsrr",
|
|
||||||
Ports: externalPort
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
Protocol: "tcp",
|
|
||||||
TargetPort: 3306,
|
|
||||||
PublishedPort: externalPort,
|
|
||||||
PublishMode: "host",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
const service = docker.getService(appName);
|
|
||||||
const inspect = await service.inspect();
|
|
||||||
await service.update({
|
|
||||||
version: Number.parseInt(inspect.Version.Index),
|
|
||||||
...settings,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
await docker.createService(settings);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
import type { InferResultType } from "@/server/types/with";
|
|
||||||
import type { CreateServiceOptions } from "dockerode";
|
|
||||||
import {
|
|
||||||
calculateResources,
|
|
||||||
generateBindMounts,
|
|
||||||
generateFileMounts,
|
|
||||||
generateVolumeMounts,
|
|
||||||
prepareEnvironmentVariables,
|
|
||||||
} from "../docker/utils";
|
|
||||||
import { getRemoteDocker } from "../servers/remote-docker";
|
|
||||||
|
|
||||||
export type MongoNested = InferResultType<"mongo", { mounts: true }>;
|
|
||||||
|
|
||||||
export const buildMongo = async (mongo: MongoNested) => {
|
|
||||||
const {
|
|
||||||
appName,
|
|
||||||
env,
|
|
||||||
externalPort,
|
|
||||||
dockerImage,
|
|
||||||
memoryLimit,
|
|
||||||
memoryReservation,
|
|
||||||
cpuLimit,
|
|
||||||
cpuReservation,
|
|
||||||
databaseUser,
|
|
||||||
databasePassword,
|
|
||||||
command,
|
|
||||||
mounts,
|
|
||||||
} = mongo;
|
|
||||||
|
|
||||||
const defaultMongoEnv = `MONGO_INITDB_ROOT_USERNAME=${databaseUser}\nMONGO_INITDB_ROOT_PASSWORD=${databasePassword}${
|
|
||||||
env ? `\n${env}` : ""
|
|
||||||
}`;
|
|
||||||
const resources = calculateResources({
|
|
||||||
memoryLimit,
|
|
||||||
memoryReservation,
|
|
||||||
cpuLimit,
|
|
||||||
cpuReservation,
|
|
||||||
});
|
|
||||||
const envVariables = prepareEnvironmentVariables(defaultMongoEnv);
|
|
||||||
const volumesMount = generateVolumeMounts(mounts);
|
|
||||||
const bindsMount = generateBindMounts(mounts);
|
|
||||||
const filesMount = generateFileMounts(appName, mongo);
|
|
||||||
|
|
||||||
const docker = await getRemoteDocker(mongo.serverId);
|
|
||||||
|
|
||||||
const settings: CreateServiceOptions = {
|
|
||||||
Name: appName,
|
|
||||||
TaskTemplate: {
|
|
||||||
ContainerSpec: {
|
|
||||||
Image: dockerImage,
|
|
||||||
Env: envVariables,
|
|
||||||
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
|
|
||||||
...(command
|
|
||||||
? {
|
|
||||||
Command: ["/bin/sh"],
|
|
||||||
Args: ["-c", command],
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
},
|
|
||||||
Networks: [{ Target: "dokploy-network" }],
|
|
||||||
Resources: {
|
|
||||||
...resources,
|
|
||||||
},
|
|
||||||
Placement: {
|
|
||||||
Constraints: ["node.role==manager"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Mode: {
|
|
||||||
Replicated: {
|
|
||||||
Replicas: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
EndpointSpec: {
|
|
||||||
Mode: "dnsrr",
|
|
||||||
Ports: externalPort
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
Protocol: "tcp",
|
|
||||||
TargetPort: 27017,
|
|
||||||
PublishedPort: externalPort,
|
|
||||||
PublishMode: "host",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
const service = docker.getService(appName);
|
|
||||||
const inspect = await service.inspect();
|
|
||||||
await service.update({
|
|
||||||
version: Number.parseInt(inspect.Version.Index),
|
|
||||||
...settings,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
await docker.createService(settings);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
import type { InferResultType } from "@/server/types/with";
|
|
||||||
import type { CreateServiceOptions } from "dockerode";
|
|
||||||
import {
|
|
||||||
calculateResources,
|
|
||||||
generateBindMounts,
|
|
||||||
generateFileMounts,
|
|
||||||
generateVolumeMounts,
|
|
||||||
prepareEnvironmentVariables,
|
|
||||||
} from "../docker/utils";
|
|
||||||
import { getRemoteDocker } from "../servers/remote-docker";
|
|
||||||
|
|
||||||
export type MysqlNested = InferResultType<"mysql", { mounts: true }>;
|
|
||||||
|
|
||||||
export const buildMysql = async (mysql: MysqlNested) => {
|
|
||||||
const {
|
|
||||||
appName,
|
|
||||||
env,
|
|
||||||
externalPort,
|
|
||||||
dockerImage,
|
|
||||||
memoryLimit,
|
|
||||||
memoryReservation,
|
|
||||||
databaseName,
|
|
||||||
databaseUser,
|
|
||||||
databasePassword,
|
|
||||||
databaseRootPassword,
|
|
||||||
cpuLimit,
|
|
||||||
cpuReservation,
|
|
||||||
command,
|
|
||||||
mounts,
|
|
||||||
} = mysql;
|
|
||||||
|
|
||||||
const defaultMysqlEnv =
|
|
||||||
databaseUser !== "root"
|
|
||||||
? `MYSQL_USER=${databaseUser}\nMYSQL_DATABASE=${databaseName}\nMYSQL_PASSWORD=${databasePassword}\nMYSQL_ROOT_PASSWORD=${databaseRootPassword}${
|
|
||||||
env ? `\n${env}` : ""
|
|
||||||
}`
|
|
||||||
: `MYSQL_DATABASE=${databaseName}\nMYSQL_ROOT_PASSWORD=${databaseRootPassword}${
|
|
||||||
env ? `\n${env}` : ""
|
|
||||||
}`;
|
|
||||||
const resources = calculateResources({
|
|
||||||
memoryLimit,
|
|
||||||
memoryReservation,
|
|
||||||
cpuLimit,
|
|
||||||
cpuReservation,
|
|
||||||
});
|
|
||||||
const envVariables = prepareEnvironmentVariables(defaultMysqlEnv);
|
|
||||||
const volumesMount = generateVolumeMounts(mounts);
|
|
||||||
const bindsMount = generateBindMounts(mounts);
|
|
||||||
const filesMount = generateFileMounts(appName, mysql);
|
|
||||||
|
|
||||||
const docker = await getRemoteDocker(mysql.serverId);
|
|
||||||
|
|
||||||
const settings: CreateServiceOptions = {
|
|
||||||
Name: appName,
|
|
||||||
TaskTemplate: {
|
|
||||||
ContainerSpec: {
|
|
||||||
Image: dockerImage,
|
|
||||||
Env: envVariables,
|
|
||||||
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
|
|
||||||
...(command
|
|
||||||
? {
|
|
||||||
Command: ["/bin/sh"],
|
|
||||||
Args: ["-c", command],
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
},
|
|
||||||
Networks: [{ Target: "dokploy-network" }],
|
|
||||||
Resources: {
|
|
||||||
...resources,
|
|
||||||
},
|
|
||||||
Placement: {
|
|
||||||
Constraints: ["node.role==manager"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Mode: {
|
|
||||||
Replicated: {
|
|
||||||
Replicas: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
EndpointSpec: {
|
|
||||||
Mode: "dnsrr",
|
|
||||||
Ports: externalPort
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
Protocol: "tcp",
|
|
||||||
TargetPort: 3306,
|
|
||||||
PublishedPort: externalPort,
|
|
||||||
PublishMode: "host",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
const service = docker.getService(appName);
|
|
||||||
const inspect = await service.inspect();
|
|
||||||
await service.update({
|
|
||||||
version: Number.parseInt(inspect.Version.Index),
|
|
||||||
...settings,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
await docker.createService(settings);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
import type { InferResultType } from "@/server/types/with";
|
|
||||||
import type { CreateServiceOptions } from "dockerode";
|
|
||||||
import {
|
|
||||||
calculateResources,
|
|
||||||
generateBindMounts,
|
|
||||||
generateFileMounts,
|
|
||||||
generateVolumeMounts,
|
|
||||||
prepareEnvironmentVariables,
|
|
||||||
} from "../docker/utils";
|
|
||||||
import { getRemoteDocker } from "../servers/remote-docker";
|
|
||||||
|
|
||||||
export type PostgresNested = InferResultType<"postgres", { mounts: true }>;
|
|
||||||
export const buildPostgres = async (postgres: PostgresNested) => {
|
|
||||||
const {
|
|
||||||
appName,
|
|
||||||
env,
|
|
||||||
externalPort,
|
|
||||||
dockerImage,
|
|
||||||
memoryLimit,
|
|
||||||
memoryReservation,
|
|
||||||
cpuLimit,
|
|
||||||
cpuReservation,
|
|
||||||
databaseName,
|
|
||||||
databaseUser,
|
|
||||||
databasePassword,
|
|
||||||
command,
|
|
||||||
mounts,
|
|
||||||
} = postgres;
|
|
||||||
|
|
||||||
const defaultPostgresEnv = `POSTGRES_DB=${databaseName}\nPOSTGRES_USER=${databaseUser}\nPOSTGRES_PASSWORD=${databasePassword}${
|
|
||||||
env ? `\n${env}` : ""
|
|
||||||
}`;
|
|
||||||
const resources = calculateResources({
|
|
||||||
memoryLimit,
|
|
||||||
memoryReservation,
|
|
||||||
cpuLimit,
|
|
||||||
cpuReservation,
|
|
||||||
});
|
|
||||||
const envVariables = prepareEnvironmentVariables(defaultPostgresEnv);
|
|
||||||
const volumesMount = generateVolumeMounts(mounts);
|
|
||||||
const bindsMount = generateBindMounts(mounts);
|
|
||||||
const filesMount = generateFileMounts(appName, postgres);
|
|
||||||
|
|
||||||
const docker = await getRemoteDocker(postgres.serverId);
|
|
||||||
|
|
||||||
const settings: CreateServiceOptions = {
|
|
||||||
Name: appName,
|
|
||||||
TaskTemplate: {
|
|
||||||
ContainerSpec: {
|
|
||||||
Image: dockerImage,
|
|
||||||
Env: envVariables,
|
|
||||||
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
|
|
||||||
...(command
|
|
||||||
? {
|
|
||||||
Command: ["/bin/sh"],
|
|
||||||
Args: ["-c", command],
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
},
|
|
||||||
Networks: [{ Target: "dokploy-network" }],
|
|
||||||
Resources: {
|
|
||||||
...resources,
|
|
||||||
},
|
|
||||||
Placement: {
|
|
||||||
Constraints: ["node.role==manager"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Mode: {
|
|
||||||
Replicated: {
|
|
||||||
Replicas: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
EndpointSpec: {
|
|
||||||
Mode: "dnsrr",
|
|
||||||
Ports: externalPort
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
Protocol: "tcp",
|
|
||||||
TargetPort: 5432,
|
|
||||||
PublishedPort: externalPort,
|
|
||||||
PublishMode: "host",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
const service = docker.getService(appName);
|
|
||||||
const inspect = await service.inspect();
|
|
||||||
await service.update({
|
|
||||||
version: Number.parseInt(inspect.Version.Index),
|
|
||||||
...settings,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.log("error", error);
|
|
||||||
await docker.createService(settings);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
import type { InferResultType } from "@/server/types/with";
|
|
||||||
import type { CreateServiceOptions } from "dockerode";
|
|
||||||
import {
|
|
||||||
calculateResources,
|
|
||||||
generateBindMounts,
|
|
||||||
generateFileMounts,
|
|
||||||
generateVolumeMounts,
|
|
||||||
prepareEnvironmentVariables,
|
|
||||||
} from "../docker/utils";
|
|
||||||
import { getRemoteDocker } from "../servers/remote-docker";
|
|
||||||
|
|
||||||
export type RedisNested = InferResultType<"redis", { mounts: true }>;
|
|
||||||
export const buildRedis = async (redis: RedisNested) => {
|
|
||||||
const {
|
|
||||||
appName,
|
|
||||||
env,
|
|
||||||
externalPort,
|
|
||||||
dockerImage,
|
|
||||||
memoryLimit,
|
|
||||||
memoryReservation,
|
|
||||||
databasePassword,
|
|
||||||
cpuLimit,
|
|
||||||
cpuReservation,
|
|
||||||
command,
|
|
||||||
mounts,
|
|
||||||
} = redis;
|
|
||||||
|
|
||||||
const defaultRedisEnv = `REDIS_PASSWORD=${databasePassword}${
|
|
||||||
env ? `\n${env}` : ""
|
|
||||||
}`;
|
|
||||||
const resources = calculateResources({
|
|
||||||
memoryLimit,
|
|
||||||
memoryReservation,
|
|
||||||
cpuLimit,
|
|
||||||
cpuReservation,
|
|
||||||
});
|
|
||||||
const envVariables = prepareEnvironmentVariables(defaultRedisEnv);
|
|
||||||
const volumesMount = generateVolumeMounts(mounts);
|
|
||||||
const bindsMount = generateBindMounts(mounts);
|
|
||||||
const filesMount = generateFileMounts(appName, redis);
|
|
||||||
|
|
||||||
const docker = await getRemoteDocker(redis.serverId);
|
|
||||||
|
|
||||||
const settings: CreateServiceOptions = {
|
|
||||||
Name: appName,
|
|
||||||
TaskTemplate: {
|
|
||||||
ContainerSpec: {
|
|
||||||
Image: dockerImage,
|
|
||||||
Env: envVariables,
|
|
||||||
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
|
|
||||||
Command: ["/bin/sh"],
|
|
||||||
Args: [
|
|
||||||
"-c",
|
|
||||||
command ? command : `redis-server --requirepass ${databasePassword}`,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
Networks: [{ Target: "dokploy-network" }],
|
|
||||||
Resources: {
|
|
||||||
...resources,
|
|
||||||
},
|
|
||||||
Placement: {
|
|
||||||
Constraints: ["node.role==manager"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Mode: {
|
|
||||||
Replicated: {
|
|
||||||
Replicas: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
EndpointSpec: {
|
|
||||||
Mode: "dnsrr",
|
|
||||||
Ports: externalPort
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
Protocol: "tcp",
|
|
||||||
TargetPort: 6379,
|
|
||||||
PublishedPort: externalPort,
|
|
||||||
PublishMode: "host",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const service = docker.getService(appName);
|
|
||||||
const inspect = await service.inspect();
|
|
||||||
await service.update({
|
|
||||||
version: Number.parseInt(inspect.Version.Index),
|
|
||||||
...settings,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
await docker.createService(settings);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { exec } from "node:child_process";
|
import { exec } from "node:child_process";
|
||||||
import util from "node:util";
|
import util from "node:util";
|
||||||
import { findServerById } from "@/server/api/services/server";
|
import { findServerById } from "@dokploy/builders";
|
||||||
import { Client } from "ssh2";
|
import { Client } from "ssh2";
|
||||||
import { readSSHKey } from "../filesystem/ssh";
|
import { readSSHKey } from "../filesystem/ssh";
|
||||||
export const execAsync = util.promisify(exec);
|
export const execAsync = util.promisify(exec);
|
||||||
|
|||||||
@@ -1,361 +0,0 @@
|
|||||||
import { createWriteStream } from "node:fs";
|
|
||||||
import { join } from "node:path";
|
|
||||||
import { findBitbucketById } from "@/server/api/services/bitbucket";
|
|
||||||
import type { Compose } from "@/server/api/services/compose";
|
|
||||||
import { paths } from "@/server/constants";
|
|
||||||
import type {
|
|
||||||
apiBitbucketTestConnection,
|
|
||||||
apiFindBitbucketBranches,
|
|
||||||
} from "@/server/db/schema";
|
|
||||||
import type { InferResultType } from "@/server/types/with";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import { recreateDirectory } from "../filesystem/directory";
|
|
||||||
import { execAsyncRemote } from "../process/execAsync";
|
|
||||||
import { spawnAsync } from "../process/spawnAsync";
|
|
||||||
|
|
||||||
export type ApplicationWithBitbucket = InferResultType<
|
|
||||||
"applications",
|
|
||||||
{ bitbucket: true }
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type ComposeWithBitbucket = InferResultType<
|
|
||||||
"compose",
|
|
||||||
{ bitbucket: true }
|
|
||||||
>;
|
|
||||||
|
|
||||||
export const cloneBitbucketRepository = async (
|
|
||||||
entity: ApplicationWithBitbucket | ComposeWithBitbucket,
|
|
||||||
logPath: string,
|
|
||||||
isCompose = false,
|
|
||||||
) => {
|
|
||||||
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths();
|
|
||||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
|
||||||
const {
|
|
||||||
appName,
|
|
||||||
bitbucketRepository,
|
|
||||||
bitbucketOwner,
|
|
||||||
bitbucketBranch,
|
|
||||||
bitbucketId,
|
|
||||||
bitbucket,
|
|
||||||
} = entity;
|
|
||||||
|
|
||||||
if (!bitbucketId) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Bitbucket Provider not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
|
||||||
const outputPath = join(basePath, appName, "code");
|
|
||||||
await recreateDirectory(outputPath);
|
|
||||||
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
|
|
||||||
const cloneUrl = `https://${bitbucket?.bitbucketUsername}:${bitbucket?.appPassword}@${repoclone}`;
|
|
||||||
try {
|
|
||||||
writeStream.write(`\nCloning Repo ${repoclone} to ${outputPath}: ✅\n`);
|
|
||||||
await spawnAsync(
|
|
||||||
"git",
|
|
||||||
[
|
|
||||||
"clone",
|
|
||||||
"--branch",
|
|
||||||
bitbucketBranch!,
|
|
||||||
"--depth",
|
|
||||||
"1",
|
|
||||||
cloneUrl,
|
|
||||||
outputPath,
|
|
||||||
"--progress",
|
|
||||||
],
|
|
||||||
(data) => {
|
|
||||||
if (writeStream.writable) {
|
|
||||||
writeStream.write(data);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
writeStream.write(`\nCloned ${repoclone} to ${outputPath}: ✅\n`);
|
|
||||||
} catch (error) {
|
|
||||||
writeStream.write(`ERROR Clonning: ${error}: ❌`);
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
writeStream.end();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const cloneRawBitbucketRepository = async (entity: Compose) => {
|
|
||||||
const { COMPOSE_PATH } = paths();
|
|
||||||
const {
|
|
||||||
appName,
|
|
||||||
bitbucketRepository,
|
|
||||||
bitbucketOwner,
|
|
||||||
bitbucketBranch,
|
|
||||||
bitbucketId,
|
|
||||||
} = entity;
|
|
||||||
|
|
||||||
if (!bitbucketId) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Bitbucket Provider not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const bitbucketProvider = await findBitbucketById(bitbucketId);
|
|
||||||
const basePath = COMPOSE_PATH;
|
|
||||||
const outputPath = join(basePath, appName, "code");
|
|
||||||
await recreateDirectory(outputPath);
|
|
||||||
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
|
|
||||||
const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await spawnAsync("git", [
|
|
||||||
"clone",
|
|
||||||
"--branch",
|
|
||||||
bitbucketBranch!,
|
|
||||||
"--depth",
|
|
||||||
"1",
|
|
||||||
cloneUrl,
|
|
||||||
outputPath,
|
|
||||||
"--progress",
|
|
||||||
]);
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const cloneRawBitbucketRepositoryRemote = async (compose: Compose) => {
|
|
||||||
const { COMPOSE_PATH } = paths(true);
|
|
||||||
const {
|
|
||||||
appName,
|
|
||||||
bitbucketRepository,
|
|
||||||
bitbucketOwner,
|
|
||||||
bitbucketBranch,
|
|
||||||
bitbucketId,
|
|
||||||
serverId,
|
|
||||||
} = compose;
|
|
||||||
|
|
||||||
if (!serverId) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Server not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!bitbucketId) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Bitbucket Provider not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const bitbucketProvider = await findBitbucketById(bitbucketId);
|
|
||||||
const basePath = COMPOSE_PATH;
|
|
||||||
const outputPath = join(basePath, appName, "code");
|
|
||||||
await recreateDirectory(outputPath);
|
|
||||||
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
|
|
||||||
const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await execAsyncRemote(
|
|
||||||
serverId,
|
|
||||||
`git clone --branch ${bitbucketBranch} --depth 1 ${cloneUrl} ${outputPath}`,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getBitbucketCloneCommand = async (
|
|
||||||
entity: ApplicationWithBitbucket | ComposeWithBitbucket,
|
|
||||||
logPath: string,
|
|
||||||
isCompose = false,
|
|
||||||
) => {
|
|
||||||
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(true);
|
|
||||||
const {
|
|
||||||
appName,
|
|
||||||
bitbucketRepository,
|
|
||||||
bitbucketOwner,
|
|
||||||
bitbucketBranch,
|
|
||||||
bitbucketId,
|
|
||||||
serverId,
|
|
||||||
bitbucket,
|
|
||||||
} = entity;
|
|
||||||
|
|
||||||
if (!serverId) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Server not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!bitbucketId) {
|
|
||||||
const command = `
|
|
||||||
echo "Error: ❌ Bitbucket Provider not found" >> ${logPath};
|
|
||||||
exit 1;
|
|
||||||
`;
|
|
||||||
await execAsyncRemote(serverId, command);
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Bitbucket Provider not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const bitbucketProvider = await findBitbucketById(bitbucketId);
|
|
||||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
|
||||||
const outputPath = join(basePath, appName, "code");
|
|
||||||
await recreateDirectory(outputPath);
|
|
||||||
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
|
|
||||||
const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`;
|
|
||||||
|
|
||||||
const cloneCommand = `
|
|
||||||
rm -rf ${outputPath};
|
|
||||||
mkdir -p ${outputPath};
|
|
||||||
if ! git clone --branch ${bitbucketBranch} --depth 1 --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
|
|
||||||
echo "❌ [ERROR] Fail to clone the repository ${repoclone}" >> ${logPath};
|
|
||||||
exit 1;
|
|
||||||
fi
|
|
||||||
echo "Cloned ${repoclone} to ${outputPath}: ✅" >> ${logPath};
|
|
||||||
`;
|
|
||||||
|
|
||||||
return cloneCommand;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getBitbucketRepositories = async (bitbucketId?: string) => {
|
|
||||||
if (!bitbucketId) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const bitbucketProvider = await findBitbucketById(bitbucketId);
|
|
||||||
|
|
||||||
const username =
|
|
||||||
bitbucketProvider.bitbucketWorkspaceName ||
|
|
||||||
bitbucketProvider.bitbucketUsername;
|
|
||||||
const url = `https://api.bitbucket.org/2.0/repositories/${username}`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Basic ${Buffer.from(`${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}`).toString("base64")}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: `Failed to fetch repositories: ${response.statusText}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
const mappedData = data.values.map((repo: any) => {
|
|
||||||
return {
|
|
||||||
name: repo.name,
|
|
||||||
url: repo.links.html.href,
|
|
||||||
owner: {
|
|
||||||
username: repo.workspace.slug,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return mappedData as {
|
|
||||||
name: string;
|
|
||||||
url: string;
|
|
||||||
owner: {
|
|
||||||
username: string;
|
|
||||||
};
|
|
||||||
}[];
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getBitbucketBranches = async (
|
|
||||||
input: typeof apiFindBitbucketBranches._type,
|
|
||||||
) => {
|
|
||||||
if (!input.bitbucketId) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
|
|
||||||
const { owner, repo } = input;
|
|
||||||
const url = `https://api.bitbucket.org/2.0/repositories/${owner}/${repo}/refs/branches`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Basic ${Buffer.from(`${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}`).toString("base64")}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: `HTTP error! status: ${response.status}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
const mappedData = data.values.map((branch: any) => {
|
|
||||||
return {
|
|
||||||
name: branch.name,
|
|
||||||
commit: {
|
|
||||||
sha: branch.target.hash,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return mappedData as {
|
|
||||||
name: string;
|
|
||||||
commit: {
|
|
||||||
sha: string;
|
|
||||||
};
|
|
||||||
}[];
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const testBitbucketConnection = async (
|
|
||||||
input: typeof apiBitbucketTestConnection._type,
|
|
||||||
) => {
|
|
||||||
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
|
|
||||||
|
|
||||||
if (!bitbucketProvider) {
|
|
||||||
throw new Error("Bitbucket provider not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
const { bitbucketUsername, workspaceName } = input;
|
|
||||||
|
|
||||||
const username = workspaceName || bitbucketUsername;
|
|
||||||
|
|
||||||
const url = `https://api.bitbucket.org/2.0/repositories/${username}`;
|
|
||||||
try {
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Basic ${Buffer.from(`${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}`).toString("base64")}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: `Failed to fetch repositories: ${response.statusText}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
const mappedData = data.values.map((repo: any) => {
|
|
||||||
return {
|
|
||||||
name: repo.name,
|
|
||||||
url: repo.links.html.href,
|
|
||||||
owner: {
|
|
||||||
username: repo.workspace.slug,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}) as [];
|
|
||||||
|
|
||||||
return mappedData.length;
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
import { createWriteStream } from "node:fs";
|
|
||||||
import { type ApplicationNested, mechanizeDockerContainer } from "../builders";
|
|
||||||
import { pullImage } from "../docker/utils";
|
|
||||||
|
|
||||||
interface RegistryAuth {
|
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
serveraddress: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const buildDocker = async (
|
|
||||||
application: ApplicationNested,
|
|
||||||
logPath: string,
|
|
||||||
): Promise<void> => {
|
|
||||||
const { buildType, dockerImage, username, password } = application;
|
|
||||||
const authConfig: Partial<RegistryAuth> = {
|
|
||||||
username: username || "",
|
|
||||||
password: password || "",
|
|
||||||
};
|
|
||||||
|
|
||||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
|
||||||
|
|
||||||
writeStream.write(`\nBuild ${buildType}\n`);
|
|
||||||
|
|
||||||
writeStream.write(`Pulling ${dockerImage}: ✅\n`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!dockerImage) {
|
|
||||||
throw new Error("Docker image not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
await pullImage(
|
|
||||||
dockerImage,
|
|
||||||
(data) => {
|
|
||||||
if (writeStream.writable) {
|
|
||||||
writeStream.write(`${data.status}\n`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
authConfig,
|
|
||||||
);
|
|
||||||
await mechanizeDockerContainer(application);
|
|
||||||
writeStream.write("\nDocker Deployed: ✅\n");
|
|
||||||
} catch (error) {
|
|
||||||
writeStream.write(`ERROR: ${error}: ❌`);
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
writeStream.end();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const buildRemoteDocker = async (
|
|
||||||
application: ApplicationNested,
|
|
||||||
logPath: string,
|
|
||||||
) => {
|
|
||||||
const { sourceType, dockerImage, username, password } = application;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!dockerImage) {
|
|
||||||
throw new Error("Docker image not found");
|
|
||||||
}
|
|
||||||
let command = `
|
|
||||||
echo "Pulling ${dockerImage}" >> ${logPath};
|
|
||||||
`;
|
|
||||||
|
|
||||||
if (username && password) {
|
|
||||||
command += `
|
|
||||||
if ! docker login --username ${username} --password ${password} https://index.docker.io/v1/ >> ${logPath} 2>&1; then
|
|
||||||
echo "❌ Login failed" >> ${logPath};
|
|
||||||
exit 1;
|
|
||||||
fi
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
command += `
|
|
||||||
docker pull ${dockerImage} >> ${logPath} 2>> ${logPath} || {
|
|
||||||
echo "❌ Pulling image failed" >> ${logPath};
|
|
||||||
exit 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "✅ Pulling image completed." >> ${logPath};
|
|
||||||
`;
|
|
||||||
return command;
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,347 +0,0 @@
|
|||||||
import { createWriteStream } from "node:fs";
|
|
||||||
import path, { join } from "node:path";
|
|
||||||
import type { Compose } from "@/server/api/services/compose";
|
|
||||||
import { updateSSHKeyById } from "@/server/api/services/ssh-key";
|
|
||||||
import { paths } from "@/server/constants";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import { recreateDirectory } from "../filesystem/directory";
|
|
||||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
|
||||||
import { spawnAsync } from "../process/spawnAsync";
|
|
||||||
|
|
||||||
export const cloneGitRepository = async (
|
|
||||||
entity: {
|
|
||||||
appName: string;
|
|
||||||
customGitUrl?: string | null;
|
|
||||||
customGitBranch?: string | null;
|
|
||||||
customGitSSHKeyId?: string | null;
|
|
||||||
},
|
|
||||||
logPath: string,
|
|
||||||
isCompose = false,
|
|
||||||
) => {
|
|
||||||
const { SSH_PATH, COMPOSE_PATH, APPLICATIONS_PATH } = paths();
|
|
||||||
const { appName, customGitUrl, customGitBranch, customGitSSHKeyId } = entity;
|
|
||||||
|
|
||||||
if (!customGitUrl || !customGitBranch) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error: Repository not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
|
||||||
const keyPath = path.join(SSH_PATH, `${customGitSSHKeyId}_rsa`);
|
|
||||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
|
||||||
const outputPath = join(basePath, appName, "code");
|
|
||||||
const knownHostsPath = path.join(SSH_PATH, "known_hosts");
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!isHttpOrHttps(customGitUrl)) {
|
|
||||||
await addHostToKnownHosts(customGitUrl);
|
|
||||||
}
|
|
||||||
await recreateDirectory(outputPath);
|
|
||||||
// const command = `GIT_SSH_COMMAND="ssh -i ${keyPath} -o UserKnownHostsFile=${knownHostsPath}" git clone --branch ${customGitBranch} --depth 1 ${customGitUrl} ${gitCopyPath} --progress`;
|
|
||||||
// const { stdout, stderr } = await execAsync(command);
|
|
||||||
writeStream.write(
|
|
||||||
`\nCloning Repo Custom ${customGitUrl} to ${outputPath}: ✅\n`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (customGitSSHKeyId) {
|
|
||||||
await updateSSHKeyById({
|
|
||||||
sshKeyId: customGitSSHKeyId,
|
|
||||||
lastUsedAt: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await spawnAsync(
|
|
||||||
"git",
|
|
||||||
[
|
|
||||||
"clone",
|
|
||||||
"--branch",
|
|
||||||
customGitBranch,
|
|
||||||
"--depth",
|
|
||||||
"1",
|
|
||||||
"--recurse-submodules",
|
|
||||||
customGitUrl,
|
|
||||||
outputPath,
|
|
||||||
"--progress",
|
|
||||||
],
|
|
||||||
(data) => {
|
|
||||||
if (writeStream.writable) {
|
|
||||||
writeStream.write(data);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
env: {
|
|
||||||
...process.env,
|
|
||||||
...(customGitSSHKeyId && {
|
|
||||||
GIT_SSH_COMMAND: `ssh -i ${keyPath} -o UserKnownHostsFile=${knownHostsPath}`,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
writeStream.write(`\nCloned Custom Git ${customGitUrl}: ✅\n`);
|
|
||||||
} catch (error) {
|
|
||||||
writeStream.write(`\nERROR Cloning Custom Git: ${error}: ❌\n`);
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
writeStream.end();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getCustomGitCloneCommand = async (
|
|
||||||
entity: {
|
|
||||||
appName: string;
|
|
||||||
customGitUrl?: string | null;
|
|
||||||
customGitBranch?: string | null;
|
|
||||||
customGitSSHKeyId?: string | null;
|
|
||||||
serverId: string | null;
|
|
||||||
},
|
|
||||||
logPath: string,
|
|
||||||
isCompose = false,
|
|
||||||
) => {
|
|
||||||
const { SSH_PATH, COMPOSE_PATH, APPLICATIONS_PATH } = paths(true);
|
|
||||||
const {
|
|
||||||
appName,
|
|
||||||
customGitUrl,
|
|
||||||
customGitBranch,
|
|
||||||
customGitSSHKeyId,
|
|
||||||
serverId,
|
|
||||||
} = entity;
|
|
||||||
|
|
||||||
if (!customGitUrl || !customGitBranch) {
|
|
||||||
const command = `
|
|
||||||
echo "Error: ❌ Repository not found" >> ${logPath};
|
|
||||||
exit 1;
|
|
||||||
`;
|
|
||||||
|
|
||||||
await execAsyncRemote(serverId, command);
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error: Repository not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const keyPath = path.join(SSH_PATH, `${customGitSSHKeyId}_rsa`);
|
|
||||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
|
||||||
const outputPath = join(basePath, appName, "code");
|
|
||||||
const knownHostsPath = path.join(SSH_PATH, "known_hosts");
|
|
||||||
|
|
||||||
if (customGitSSHKeyId) {
|
|
||||||
await updateSSHKeyById({
|
|
||||||
sshKeyId: customGitSSHKeyId,
|
|
||||||
lastUsedAt: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const command = [];
|
|
||||||
if (!isHttpOrHttps(customGitUrl)) {
|
|
||||||
command.push(addHostToKnownHostsCommand(customGitUrl));
|
|
||||||
}
|
|
||||||
command.push(`rm -rf ${outputPath};`);
|
|
||||||
command.push(`mkdir -p ${outputPath};`);
|
|
||||||
command.push(
|
|
||||||
`echo "Cloning Custom Git ${customGitUrl}" to ${outputPath}: ✅ >> ${logPath};`,
|
|
||||||
);
|
|
||||||
if (customGitSSHKeyId) {
|
|
||||||
command.push(
|
|
||||||
`GIT_SSH_COMMAND="ssh -i ${keyPath} -o UserKnownHostsFile=${knownHostsPath}"`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
command.push(
|
|
||||||
`if ! git clone --branch ${customGitBranch} --depth 1 --progress ${customGitUrl} ${outputPath} >> ${logPath} 2>&1; then
|
|
||||||
echo "❌ [ERROR] Fail to clone the repository ${customGitUrl}" >> ${logPath};
|
|
||||||
exit 1;
|
|
||||||
fi
|
|
||||||
`,
|
|
||||||
);
|
|
||||||
command.push(`echo "Cloned Custom Git ${customGitUrl}: ✅" >> ${logPath};`);
|
|
||||||
return command.join("\n");
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isHttpOrHttps = (url: string): boolean => {
|
|
||||||
const regex = /^https?:\/\//;
|
|
||||||
return regex.test(url);
|
|
||||||
};
|
|
||||||
|
|
||||||
const addHostToKnownHosts = async (repositoryURL: string) => {
|
|
||||||
const { SSH_PATH } = paths();
|
|
||||||
const { domain, port } = sanitizeRepoPathSSH(repositoryURL);
|
|
||||||
const knownHostsPath = path.join(SSH_PATH, "known_hosts");
|
|
||||||
|
|
||||||
const command = `ssh-keyscan -p ${port} ${domain} >> ${knownHostsPath}`;
|
|
||||||
try {
|
|
||||||
await execAsync(command);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error adding host to known_hosts: ${error}`);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const addHostToKnownHostsCommand = (repositoryURL: string) => {
|
|
||||||
const { SSH_PATH } = paths();
|
|
||||||
const { domain, port } = sanitizeRepoPathSSH(repositoryURL);
|
|
||||||
const knownHostsPath = path.join(SSH_PATH, "known_hosts");
|
|
||||||
|
|
||||||
return `ssh-keyscan -p ${port} ${domain} >> ${knownHostsPath};`;
|
|
||||||
};
|
|
||||||
const sanitizeRepoPathSSH = (input: string) => {
|
|
||||||
const SSH_PATH_RE = new RegExp(
|
|
||||||
[
|
|
||||||
/^\s*/,
|
|
||||||
/(?:(?<proto>[a-z]+):\/\/)?/,
|
|
||||||
/(?:(?<user>[a-z_][a-z0-9_-]+)@)?/,
|
|
||||||
/(?<domain>[^\s\/\?#:]+)/,
|
|
||||||
/(?::(?<port>[0-9]{1,5}))?/,
|
|
||||||
/(?:[\/:](?<owner>[^\s\/\?#:]+))?/,
|
|
||||||
/(?:[\/:](?<repo>(?:[^\s\?#:.]|\.(?!git\/?\s*$))+))/,
|
|
||||||
/(?:.git)?\/?\s*$/,
|
|
||||||
]
|
|
||||||
.map((r) => r.source)
|
|
||||||
.join(""),
|
|
||||||
"i",
|
|
||||||
);
|
|
||||||
|
|
||||||
const found = input.match(SSH_PATH_RE);
|
|
||||||
if (!found) {
|
|
||||||
throw new Error(`Malformatted SSH path: ${input}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
user: found.groups?.user ?? "git",
|
|
||||||
domain: found.groups?.domain,
|
|
||||||
port: Number(found.groups?.port ?? 22),
|
|
||||||
owner: found.groups?.owner ?? "",
|
|
||||||
repo: found.groups?.repo,
|
|
||||||
get repoPath() {
|
|
||||||
return `ssh://${this.user}@${this.domain}:${this.port}/${this.owner}${
|
|
||||||
this.owner && "/"
|
|
||||||
}${this.repo}.git`;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const cloneGitRawRepository = async (entity: {
|
|
||||||
appName: string;
|
|
||||||
customGitUrl?: string | null;
|
|
||||||
customGitBranch?: string | null;
|
|
||||||
customGitSSHKeyId?: string | null;
|
|
||||||
}) => {
|
|
||||||
const { appName, customGitUrl, customGitBranch, customGitSSHKeyId } = entity;
|
|
||||||
|
|
||||||
if (!customGitUrl || !customGitBranch) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error: Repository not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { SSH_PATH, COMPOSE_PATH } = paths();
|
|
||||||
const keyPath = path.join(SSH_PATH, `${customGitSSHKeyId}_rsa`);
|
|
||||||
const basePath = COMPOSE_PATH;
|
|
||||||
const outputPath = join(basePath, appName, "code");
|
|
||||||
const knownHostsPath = path.join(SSH_PATH, "known_hosts");
|
|
||||||
|
|
||||||
try {
|
|
||||||
await addHostToKnownHosts(customGitUrl);
|
|
||||||
await recreateDirectory(outputPath);
|
|
||||||
|
|
||||||
if (customGitSSHKeyId) {
|
|
||||||
await updateSSHKeyById({
|
|
||||||
sshKeyId: customGitSSHKeyId,
|
|
||||||
lastUsedAt: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await spawnAsync(
|
|
||||||
"git",
|
|
||||||
[
|
|
||||||
"clone",
|
|
||||||
"--branch",
|
|
||||||
customGitBranch,
|
|
||||||
"--depth",
|
|
||||||
"1",
|
|
||||||
customGitUrl,
|
|
||||||
outputPath,
|
|
||||||
"--progress",
|
|
||||||
],
|
|
||||||
(data) => {},
|
|
||||||
{
|
|
||||||
env: {
|
|
||||||
...process.env,
|
|
||||||
...(customGitSSHKeyId && {
|
|
||||||
GIT_SSH_COMMAND: `ssh -i ${keyPath} -o UserKnownHostsFile=${knownHostsPath}`,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const cloneRawGitRepositoryRemote = async (compose: Compose) => {
|
|
||||||
const {
|
|
||||||
appName,
|
|
||||||
customGitBranch,
|
|
||||||
customGitUrl,
|
|
||||||
customGitSSHKeyId,
|
|
||||||
serverId,
|
|
||||||
} = compose;
|
|
||||||
|
|
||||||
if (!serverId) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Server not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!customGitUrl) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Git Provider not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { SSH_PATH, COMPOSE_PATH } = paths(true);
|
|
||||||
const keyPath = path.join(SSH_PATH, `${customGitSSHKeyId}_rsa`);
|
|
||||||
const basePath = COMPOSE_PATH;
|
|
||||||
const outputPath = join(basePath, appName, "code");
|
|
||||||
const knownHostsPath = path.join(SSH_PATH, "known_hosts");
|
|
||||||
|
|
||||||
if (customGitSSHKeyId) {
|
|
||||||
await updateSSHKeyById({
|
|
||||||
sshKeyId: customGitSSHKeyId,
|
|
||||||
lastUsedAt: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const command = [];
|
|
||||||
if (!isHttpOrHttps(customGitUrl)) {
|
|
||||||
command.push(addHostToKnownHostsCommand(customGitUrl));
|
|
||||||
}
|
|
||||||
command.push(`rm -rf ${outputPath};`);
|
|
||||||
command.push(`mkdir -p ${outputPath};`);
|
|
||||||
if (customGitSSHKeyId) {
|
|
||||||
command.push(
|
|
||||||
`GIT_SSH_COMMAND="ssh -i ${keyPath} -o UserKnownHostsFile=${knownHostsPath}"`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
command.push(
|
|
||||||
`if ! git clone --branch ${customGitBranch} --depth 1 --progress ${customGitUrl} ${outputPath} ; then
|
|
||||||
echo "[ERROR] Fail to clone the repository ";
|
|
||||||
exit 1;
|
|
||||||
fi
|
|
||||||
`,
|
|
||||||
);
|
|
||||||
|
|
||||||
await execAsyncRemote(serverId, command.join("\n"));
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,336 +0,0 @@
|
|||||||
import { createWriteStream } from "node:fs";
|
|
||||||
import { join } from "node:path";
|
|
||||||
import { paths } from "@/server/constants";
|
|
||||||
import type { InferResultType } from "@/server/types/with";
|
|
||||||
import { createAppAuth } from "@octokit/auth-app";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import { Octokit } from "octokit";
|
|
||||||
import { recreateDirectory } from "../filesystem/directory";
|
|
||||||
import { spawnAsync } from "../process/spawnAsync";
|
|
||||||
|
|
||||||
import type { Compose } from "@/server/api/services/compose";
|
|
||||||
import { type Github, findGithubById } from "@/server/api/services/github";
|
|
||||||
import type { apiFindGithubBranches } from "@/server/db/schema";
|
|
||||||
import { execAsyncRemote } from "../process/execAsync";
|
|
||||||
|
|
||||||
export const authGithub = (githubProvider: Github) => {
|
|
||||||
if (!haveGithubRequirements(githubProvider)) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Github Account not configured correctly",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const octokit = new Octokit({
|
|
||||||
authStrategy: createAppAuth,
|
|
||||||
auth: {
|
|
||||||
appId: githubProvider?.githubAppId || 0,
|
|
||||||
privateKey: githubProvider?.githubPrivateKey || "",
|
|
||||||
installationId: githubProvider?.githubInstallationId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return octokit;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getGithubToken = async (
|
|
||||||
octokit: ReturnType<typeof authGithub>,
|
|
||||||
) => {
|
|
||||||
const installation = (await octokit.auth({
|
|
||||||
type: "installation",
|
|
||||||
})) as {
|
|
||||||
token: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
return installation.token;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const haveGithubRequirements = (githubProvider: Github) => {
|
|
||||||
return !!(
|
|
||||||
githubProvider?.githubAppId &&
|
|
||||||
githubProvider?.githubPrivateKey &&
|
|
||||||
githubProvider?.githubInstallationId
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getErrorCloneRequirements = (entity: {
|
|
||||||
repository?: string | null;
|
|
||||||
owner?: string | null;
|
|
||||||
branch?: string | null;
|
|
||||||
}) => {
|
|
||||||
const reasons: string[] = [];
|
|
||||||
const { repository, owner, branch } = entity;
|
|
||||||
|
|
||||||
if (!repository) reasons.push("1. Repository not assigned.");
|
|
||||||
if (!owner) reasons.push("2. Owner not specified.");
|
|
||||||
if (!branch) reasons.push("3. Branch not defined.");
|
|
||||||
|
|
||||||
return reasons;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ApplicationWithGithub = InferResultType<
|
|
||||||
"applications",
|
|
||||||
{ github: true }
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type ComposeWithGithub = InferResultType<"compose", { github: true }>;
|
|
||||||
export const cloneGithubRepository = async (
|
|
||||||
entity: ApplicationWithGithub | ComposeWithGithub,
|
|
||||||
logPath: string,
|
|
||||||
isCompose = false,
|
|
||||||
) => {
|
|
||||||
const { APPLICATIONS_PATH, COMPOSE_PATH } = paths();
|
|
||||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
|
||||||
const { appName, repository, owner, branch, githubId } = entity;
|
|
||||||
|
|
||||||
if (!githubId) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "GitHub Provider not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const requirements = getErrorCloneRequirements(entity);
|
|
||||||
|
|
||||||
// Check if requirements are met
|
|
||||||
if (requirements.length > 0) {
|
|
||||||
writeStream.write(
|
|
||||||
`\nGitHub Repository configuration failed for application: ${appName}\n`,
|
|
||||||
);
|
|
||||||
writeStream.write("Reasons:\n");
|
|
||||||
writeStream.write(requirements.join("\n"));
|
|
||||||
writeStream.end();
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error: GitHub repository information is incomplete.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const githubProvider = await findGithubById(githubId);
|
|
||||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
|
||||||
const outputPath = join(basePath, appName, "code");
|
|
||||||
const octokit = authGithub(githubProvider);
|
|
||||||
const token = await getGithubToken(octokit);
|
|
||||||
const repoclone = `github.com/${owner}/${repository}.git`;
|
|
||||||
await recreateDirectory(outputPath);
|
|
||||||
const cloneUrl = `https://oauth2:${token}@${repoclone}`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
writeStream.write(`\nClonning Repo ${repoclone} to ${outputPath}: ✅\n`);
|
|
||||||
await spawnAsync(
|
|
||||||
"git",
|
|
||||||
[
|
|
||||||
"clone",
|
|
||||||
"--branch",
|
|
||||||
branch!,
|
|
||||||
"--depth",
|
|
||||||
"1",
|
|
||||||
cloneUrl,
|
|
||||||
outputPath,
|
|
||||||
"--progress",
|
|
||||||
],
|
|
||||||
(data) => {
|
|
||||||
if (writeStream.writable) {
|
|
||||||
writeStream.write(data);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
writeStream.write(`\nCloned ${repoclone}: ✅\n`);
|
|
||||||
} catch (error) {
|
|
||||||
writeStream.write(`ERROR Clonning: ${error}: ❌`);
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
writeStream.end();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getGithubCloneCommand = async (
|
|
||||||
entity: ApplicationWithGithub | ComposeWithGithub,
|
|
||||||
logPath: string,
|
|
||||||
isCompose = false,
|
|
||||||
) => {
|
|
||||||
const { appName, repository, owner, branch, githubId, serverId } = entity;
|
|
||||||
|
|
||||||
if (!serverId) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Server not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!githubId) {
|
|
||||||
const command = `
|
|
||||||
echo "Error: ❌ Github Provider not found" >> ${logPath};
|
|
||||||
exit 1;
|
|
||||||
`;
|
|
||||||
|
|
||||||
await execAsyncRemote(serverId, command);
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "GitHub Provider not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const requirements = getErrorCloneRequirements(entity);
|
|
||||||
|
|
||||||
// Build log messages
|
|
||||||
let logMessages = "";
|
|
||||||
if (requirements.length > 0) {
|
|
||||||
logMessages += `\nGitHub Repository configuration failed for application: ${appName}\n`;
|
|
||||||
logMessages += "Reasons:\n";
|
|
||||||
logMessages += requirements.join("\n");
|
|
||||||
const escapedLogMessages = logMessages
|
|
||||||
.replace(/\\/g, "\\\\")
|
|
||||||
.replace(/"/g, '\\"')
|
|
||||||
.replace(/\n/g, "\\n");
|
|
||||||
|
|
||||||
const bashCommand = `
|
|
||||||
echo "${escapedLogMessages}" >> ${logPath};
|
|
||||||
exit 1; # Exit with error code
|
|
||||||
`;
|
|
||||||
|
|
||||||
await execAsyncRemote(serverId, bashCommand);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(true);
|
|
||||||
const githubProvider = await findGithubById(githubId);
|
|
||||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
|
||||||
const outputPath = join(basePath, appName, "code");
|
|
||||||
const octokit = authGithub(githubProvider);
|
|
||||||
const token = await getGithubToken(octokit);
|
|
||||||
const repoclone = `github.com/${owner}/${repository}.git`;
|
|
||||||
const cloneUrl = `https://oauth2:${token}@${repoclone}`;
|
|
||||||
|
|
||||||
const cloneCommand = `
|
|
||||||
rm -rf ${outputPath};
|
|
||||||
mkdir -p ${outputPath};
|
|
||||||
if ! git clone --branch ${branch} --depth 1 --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
|
|
||||||
echo "❌ [ERROR] Fallo al clonar el repositorio ${repoclone}" >> ${logPath};
|
|
||||||
exit 1;
|
|
||||||
fi
|
|
||||||
echo "Cloned ${repoclone} to ${outputPath}: ✅" >> ${logPath};
|
|
||||||
`;
|
|
||||||
|
|
||||||
return cloneCommand;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const cloneRawGithubRepository = async (entity: Compose) => {
|
|
||||||
const { appName, repository, owner, branch, githubId } = entity;
|
|
||||||
|
|
||||||
if (!githubId) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "GitHub Provider not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const { COMPOSE_PATH } = paths();
|
|
||||||
const githubProvider = await findGithubById(githubId);
|
|
||||||
const basePath = COMPOSE_PATH;
|
|
||||||
const outputPath = join(basePath, appName, "code");
|
|
||||||
const octokit = authGithub(githubProvider);
|
|
||||||
const token = await getGithubToken(octokit);
|
|
||||||
const repoclone = `github.com/${owner}/${repository}.git`;
|
|
||||||
await recreateDirectory(outputPath);
|
|
||||||
const cloneUrl = `https://oauth2:${token}@${repoclone}`;
|
|
||||||
try {
|
|
||||||
await spawnAsync("git", [
|
|
||||||
"clone",
|
|
||||||
"--branch",
|
|
||||||
branch!,
|
|
||||||
"--depth",
|
|
||||||
"1",
|
|
||||||
cloneUrl,
|
|
||||||
outputPath,
|
|
||||||
"--progress",
|
|
||||||
]);
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const cloneRawGithubRepositoryRemote = async (compose: Compose) => {
|
|
||||||
const { appName, repository, owner, branch, githubId, serverId } = compose;
|
|
||||||
|
|
||||||
if (!serverId) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Server not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!githubId) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "GitHub Provider not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { COMPOSE_PATH } = paths(true);
|
|
||||||
const githubProvider = await findGithubById(githubId);
|
|
||||||
const basePath = COMPOSE_PATH;
|
|
||||||
const outputPath = join(basePath, appName, "code");
|
|
||||||
const octokit = authGithub(githubProvider);
|
|
||||||
const token = await getGithubToken(octokit);
|
|
||||||
const repoclone = `github.com/${owner}/${repository}.git`;
|
|
||||||
await recreateDirectory(outputPath);
|
|
||||||
const cloneUrl = `https://oauth2:${token}@${repoclone}`;
|
|
||||||
try {
|
|
||||||
await execAsyncRemote(
|
|
||||||
serverId,
|
|
||||||
`git clone --branch ${branch} --depth 1 ${cloneUrl} ${outputPath}`,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getGithubRepositories = async (githubId?: string) => {
|
|
||||||
if (!githubId) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const githubProvider = await findGithubById(githubId);
|
|
||||||
|
|
||||||
const octokit = new Octokit({
|
|
||||||
authStrategy: createAppAuth,
|
|
||||||
auth: {
|
|
||||||
appId: githubProvider.githubAppId,
|
|
||||||
privateKey: githubProvider.githubPrivateKey,
|
|
||||||
installationId: githubProvider.githubInstallationId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const repositories = (await octokit.paginate(
|
|
||||||
octokit.rest.apps.listReposAccessibleToInstallation,
|
|
||||||
)) as unknown as Awaited<
|
|
||||||
ReturnType<typeof octokit.rest.apps.listReposAccessibleToInstallation>
|
|
||||||
>["data"]["repositories"];
|
|
||||||
|
|
||||||
return repositories;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getGithubBranches = async (
|
|
||||||
input: typeof apiFindGithubBranches._type,
|
|
||||||
) => {
|
|
||||||
if (!input.githubId) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const githubProvider = await findGithubById(input.githubId);
|
|
||||||
|
|
||||||
const octokit = new Octokit({
|
|
||||||
authStrategy: createAppAuth,
|
|
||||||
auth: {
|
|
||||||
appId: githubProvider.githubAppId,
|
|
||||||
privateKey: githubProvider.githubPrivateKey,
|
|
||||||
installationId: githubProvider.githubInstallationId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
@@ -1,447 +0,0 @@
|
|||||||
import { createWriteStream } from "node:fs";
|
|
||||||
import { join } from "node:path";
|
|
||||||
import type { Compose } from "@/server/api/services/compose";
|
|
||||||
import {
|
|
||||||
type Gitlab,
|
|
||||||
findGitlabById,
|
|
||||||
updateGitlab,
|
|
||||||
} from "@/server/api/services/gitlab";
|
|
||||||
import { paths } from "@/server/constants";
|
|
||||||
import type { apiGitlabTestConnection } from "@/server/db/schema";
|
|
||||||
import type { InferResultType } from "@/server/types/with";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import { recreateDirectory } from "../filesystem/directory";
|
|
||||||
import { execAsyncRemote } from "../process/execAsync";
|
|
||||||
import { spawnAsync } from "../process/spawnAsync";
|
|
||||||
|
|
||||||
export const refreshGitlabToken = async (gitlabProviderId: string) => {
|
|
||||||
const gitlabProvider = await findGitlabById(gitlabProviderId);
|
|
||||||
const currentTime = Math.floor(Date.now() / 1000);
|
|
||||||
|
|
||||||
const safetyMargin = 60;
|
|
||||||
if (
|
|
||||||
gitlabProvider.expiresAt &&
|
|
||||||
currentTime + safetyMargin < gitlabProvider.expiresAt
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch("https://gitlab.com/oauth/token", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
|
||||||
},
|
|
||||||
body: new URLSearchParams({
|
|
||||||
grant_type: "refresh_token",
|
|
||||||
refresh_token: gitlabProvider.refreshToken as string,
|
|
||||||
client_id: gitlabProvider.applicationId as string,
|
|
||||||
client_secret: gitlabProvider.secret as string,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to refresh token: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
const expiresAt = Math.floor(Date.now() / 1000) + data.expires_in;
|
|
||||||
|
|
||||||
console.log("Refreshed token");
|
|
||||||
|
|
||||||
await updateGitlab(gitlabProviderId, {
|
|
||||||
accessToken: data.access_token,
|
|
||||||
refreshToken: data.refresh_token,
|
|
||||||
expiresAt,
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const haveGitlabRequirements = (gitlabProvider: Gitlab) => {
|
|
||||||
return !!(gitlabProvider?.accessToken && gitlabProvider?.refreshToken);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getErrorCloneRequirements = (entity: {
|
|
||||||
gitlabRepository?: string | null;
|
|
||||||
gitlabOwner?: string | null;
|
|
||||||
gitlabBranch?: string | null;
|
|
||||||
gitlabPathNamespace?: string | null;
|
|
||||||
}) => {
|
|
||||||
const reasons: string[] = [];
|
|
||||||
const { gitlabBranch, gitlabOwner, gitlabRepository, gitlabPathNamespace } =
|
|
||||||
entity;
|
|
||||||
|
|
||||||
if (!gitlabRepository) reasons.push("1. Repository not assigned.");
|
|
||||||
if (!gitlabOwner) reasons.push("2. Owner not specified.");
|
|
||||||
if (!gitlabBranch) reasons.push("3. Branch not defined.");
|
|
||||||
if (!gitlabPathNamespace) reasons.push("4. Path namespace not defined.");
|
|
||||||
|
|
||||||
return reasons;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ApplicationWithGitlab = InferResultType<
|
|
||||||
"applications",
|
|
||||||
{ gitlab: true }
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type ComposeWithGitlab = InferResultType<"compose", { gitlab: true }>;
|
|
||||||
|
|
||||||
export const cloneGitlabRepository = async (
|
|
||||||
entity: ApplicationWithGitlab | ComposeWithGitlab,
|
|
||||||
logPath: string,
|
|
||||||
isCompose = false,
|
|
||||||
) => {
|
|
||||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
|
||||||
const { appName, gitlabBranch, gitlabId, gitlab, gitlabPathNamespace } =
|
|
||||||
entity;
|
|
||||||
|
|
||||||
if (!gitlabId) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Gitlab Provider not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await refreshGitlabToken(gitlabId);
|
|
||||||
|
|
||||||
const requirements = getErrorCloneRequirements(entity);
|
|
||||||
|
|
||||||
// Check if requirements are met
|
|
||||||
if (requirements.length > 0) {
|
|
||||||
writeStream.write(
|
|
||||||
`\nGitLab Repository configuration failed for application: ${appName}\n`,
|
|
||||||
);
|
|
||||||
writeStream.write("Reasons:\n");
|
|
||||||
writeStream.write(requirements.join("\n"));
|
|
||||||
writeStream.end();
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Error: GitLab repository information is incomplete.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths();
|
|
||||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
|
||||||
const outputPath = join(basePath, appName, "code");
|
|
||||||
await recreateDirectory(outputPath);
|
|
||||||
const repoclone = `gitlab.com/${gitlabPathNamespace}.git`;
|
|
||||||
const cloneUrl = `https://oauth2:${gitlab?.accessToken}@${repoclone}`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
writeStream.write(`\nClonning Repo ${repoclone} to ${outputPath}: ✅\n`);
|
|
||||||
await spawnAsync(
|
|
||||||
"git",
|
|
||||||
[
|
|
||||||
"clone",
|
|
||||||
"--branch",
|
|
||||||
gitlabBranch!,
|
|
||||||
"--depth",
|
|
||||||
"1",
|
|
||||||
cloneUrl,
|
|
||||||
outputPath,
|
|
||||||
"--progress",
|
|
||||||
],
|
|
||||||
(data) => {
|
|
||||||
if (writeStream.writable) {
|
|
||||||
writeStream.write(data);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
writeStream.write(`\nCloned ${repoclone}: ✅\n`);
|
|
||||||
} catch (error) {
|
|
||||||
writeStream.write(`ERROR Clonning: ${error}: ❌`);
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
writeStream.end();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getGitlabCloneCommand = async (
|
|
||||||
entity: ApplicationWithGitlab | ComposeWithGitlab,
|
|
||||||
logPath: string,
|
|
||||||
isCompose = false,
|
|
||||||
) => {
|
|
||||||
const {
|
|
||||||
appName,
|
|
||||||
gitlabRepository,
|
|
||||||
gitlabOwner,
|
|
||||||
gitlabPathNamespace,
|
|
||||||
gitlabBranch,
|
|
||||||
gitlabId,
|
|
||||||
serverId,
|
|
||||||
gitlab,
|
|
||||||
} = entity;
|
|
||||||
|
|
||||||
if (!serverId) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Server not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!gitlabId) {
|
|
||||||
const command = `
|
|
||||||
echo "Error: ❌ Gitlab Provider not found" >> ${logPath};
|
|
||||||
exit 1;
|
|
||||||
`;
|
|
||||||
|
|
||||||
await execAsyncRemote(serverId, command);
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Gitlab Provider not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const requirements = getErrorCloneRequirements(entity);
|
|
||||||
|
|
||||||
// Build log messages
|
|
||||||
let logMessages = "";
|
|
||||||
if (requirements.length > 0) {
|
|
||||||
logMessages += `\nGitLab Repository configuration failed for application: ${appName}\n`;
|
|
||||||
logMessages += "Reasons:\n";
|
|
||||||
logMessages += requirements.join("\n");
|
|
||||||
const escapedLogMessages = logMessages
|
|
||||||
.replace(/\\/g, "\\\\")
|
|
||||||
.replace(/"/g, '\\"')
|
|
||||||
.replace(/\n/g, "\\n");
|
|
||||||
|
|
||||||
const bashCommand = `
|
|
||||||
echo "${escapedLogMessages}" >> ${logPath};
|
|
||||||
exit 1; # Exit with error code
|
|
||||||
`;
|
|
||||||
|
|
||||||
await execAsyncRemote(serverId, bashCommand);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(true);
|
|
||||||
await refreshGitlabToken(gitlabId);
|
|
||||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
|
||||||
const outputPath = join(basePath, appName, "code");
|
|
||||||
await recreateDirectory(outputPath);
|
|
||||||
const repoclone = `gitlab.com/${gitlabPathNamespace}.git`;
|
|
||||||
const cloneUrl = `https://oauth2:${gitlab?.accessToken}@${repoclone}`;
|
|
||||||
|
|
||||||
const cloneCommand = `
|
|
||||||
rm -rf ${outputPath};
|
|
||||||
mkdir -p ${outputPath};
|
|
||||||
if ! git clone --branch ${gitlabBranch} --depth 1 --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
|
|
||||||
echo "❌ [ERROR] Fail to clone the repository ${repoclone}" >> ${logPath};
|
|
||||||
exit 1;
|
|
||||||
fi
|
|
||||||
echo "Cloned ${repoclone} to ${outputPath}: ✅" >> ${logPath};
|
|
||||||
`;
|
|
||||||
|
|
||||||
return cloneCommand;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getGitlabRepositories = async (gitlabId?: string) => {
|
|
||||||
if (!gitlabId) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
await refreshGitlabToken(gitlabId);
|
|
||||||
|
|
||||||
const gitlabProvider = await findGitlabById(gitlabId);
|
|
||||||
|
|
||||||
const response = await fetch(
|
|
||||||
`https://gitlab.com/api/v4/projects?membership=true&owned=true&page=${0}&per_page=${100}`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${gitlabProvider.accessToken}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: `Failed to fetch repositories: ${response.statusText}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const repositories = await response.json();
|
|
||||||
|
|
||||||
const filteredRepos = repositories.filter((repo: any) => {
|
|
||||||
const { full_path, kind } = repo.namespace;
|
|
||||||
const groupName = gitlabProvider.groupName?.toLowerCase();
|
|
||||||
|
|
||||||
if (groupName) {
|
|
||||||
return full_path.toLowerCase().includes(groupName) && kind === "group";
|
|
||||||
}
|
|
||||||
return kind === "user";
|
|
||||||
});
|
|
||||||
const mappedRepositories = filteredRepos.map((repo: any) => {
|
|
||||||
return {
|
|
||||||
id: repo.id,
|
|
||||||
name: repo.name,
|
|
||||||
url: repo.path_with_namespace,
|
|
||||||
owner: {
|
|
||||||
username: repo.namespace.path,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return mappedRepositories as {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
url: string;
|
|
||||||
owner: {
|
|
||||||
username: string;
|
|
||||||
};
|
|
||||||
}[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getGitlabBranches = async (input: {
|
|
||||||
id?: number;
|
|
||||||
gitlabId?: string;
|
|
||||||
owner: string;
|
|
||||||
repo: string;
|
|
||||||
}) => {
|
|
||||||
if (!input.gitlabId || !input.id || input.id === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const gitlabProvider = await findGitlabById(input.gitlabId);
|
|
||||||
|
|
||||||
const branchesResponse = await fetch(
|
|
||||||
`https://gitlab.com/api/v4/projects/${input.id}/repository/branches`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${gitlabProvider.accessToken}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!branchesResponse.ok) {
|
|
||||||
throw new Error(`Failed to fetch branches: ${branchesResponse.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const branches = await branchesResponse.json();
|
|
||||||
|
|
||||||
return branches as {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
commit: {
|
|
||||||
id: string;
|
|
||||||
};
|
|
||||||
}[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const cloneRawGitlabRepository = async (entity: Compose) => {
|
|
||||||
const {
|
|
||||||
appName,
|
|
||||||
gitlabRepository,
|
|
||||||
gitlabOwner,
|
|
||||||
gitlabBranch,
|
|
||||||
gitlabId,
|
|
||||||
gitlabPathNamespace,
|
|
||||||
} = entity;
|
|
||||||
|
|
||||||
if (!gitlabId) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Gitlab Provider not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const gitlabProvider = await findGitlabById(gitlabId);
|
|
||||||
const { COMPOSE_PATH } = paths();
|
|
||||||
await refreshGitlabToken(gitlabId);
|
|
||||||
const basePath = COMPOSE_PATH;
|
|
||||||
const outputPath = join(basePath, appName, "code");
|
|
||||||
await recreateDirectory(outputPath);
|
|
||||||
const repoclone = `gitlab.com/${gitlabPathNamespace}.git`;
|
|
||||||
const cloneUrl = `https://oauth2:${gitlabProvider?.accessToken}@${repoclone}`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await spawnAsync("git", [
|
|
||||||
"clone",
|
|
||||||
"--branch",
|
|
||||||
gitlabBranch!,
|
|
||||||
"--depth",
|
|
||||||
"1",
|
|
||||||
cloneUrl,
|
|
||||||
outputPath,
|
|
||||||
"--progress",
|
|
||||||
]);
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const cloneRawGitlabRepositoryRemote = async (compose: Compose) => {
|
|
||||||
const { appName, gitlabPathNamespace, branch, gitlabId, serverId } = compose;
|
|
||||||
|
|
||||||
if (!serverId) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Server not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!gitlabId) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Gitlab Provider not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const gitlabProvider = await findGitlabById(gitlabId);
|
|
||||||
const { COMPOSE_PATH } = paths(true);
|
|
||||||
await refreshGitlabToken(gitlabId);
|
|
||||||
const basePath = COMPOSE_PATH;
|
|
||||||
const outputPath = join(basePath, appName, "code");
|
|
||||||
await recreateDirectory(outputPath);
|
|
||||||
const repoclone = `gitlab.com/${gitlabPathNamespace}.git`;
|
|
||||||
const cloneUrl = `https://oauth2:${gitlabProvider?.accessToken}@${repoclone}`;
|
|
||||||
try {
|
|
||||||
await execAsyncRemote(
|
|
||||||
serverId,
|
|
||||||
`git clone --branch ${branch} --depth 1 ${cloneUrl} ${outputPath}`,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const testGitlabConnection = async (
|
|
||||||
input: typeof apiGitlabTestConnection._type,
|
|
||||||
) => {
|
|
||||||
const { gitlabId, groupName } = input;
|
|
||||||
|
|
||||||
if (!gitlabId) {
|
|
||||||
throw new Error("Gitlab provider not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
await refreshGitlabToken(gitlabId);
|
|
||||||
|
|
||||||
const gitlabProvider = await findGitlabById(gitlabId);
|
|
||||||
|
|
||||||
const response = await fetch(
|
|
||||||
`https://gitlab.com/api/v4/projects?membership=true&owned=true&page=${0}&per_page=${100}`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${gitlabProvider.accessToken}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: `Failed to fetch repositories: ${response.statusText}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const repositories = await response.json();
|
|
||||||
|
|
||||||
const filteredRepos = repositories.filter((repo: any) => {
|
|
||||||
const { full_path, kind } = repo.namespace;
|
|
||||||
|
|
||||||
if (groupName) {
|
|
||||||
return full_path.toLowerCase().includes(groupName) && kind === "group";
|
|
||||||
}
|
|
||||||
return kind === "user";
|
|
||||||
});
|
|
||||||
|
|
||||||
return filteredRepos.length;
|
|
||||||
};
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
import { createWriteStream } from "node:fs";
|
|
||||||
import { writeFile } from "node:fs/promises";
|
|
||||||
import { join } from "node:path";
|
|
||||||
import type { Compose } from "@/server/api/services/compose";
|
|
||||||
import { paths } from "@/server/constants";
|
|
||||||
import { encodeBase64 } from "../docker/utils";
|
|
||||||
import { recreateDirectory } from "../filesystem/directory";
|
|
||||||
import { execAsyncRemote } from "../process/execAsync";
|
|
||||||
|
|
||||||
export const createComposeFile = async (compose: Compose, logPath: string) => {
|
|
||||||
const { COMPOSE_PATH } = paths();
|
|
||||||
const { appName, composeFile } = compose;
|
|
||||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
|
||||||
const outputPath = join(COMPOSE_PATH, appName, "code");
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getCreateComposeFileCommand = (
|
|
||||||
compose: Compose,
|
|
||||||
logPath: string,
|
|
||||||
) => {
|
|
||||||
const { COMPOSE_PATH } = paths(true);
|
|
||||||
const { appName, composeFile } = compose;
|
|
||||||
const outputPath = join(COMPOSE_PATH, appName, "code");
|
|
||||||
const filePath = join(outputPath, "docker-compose.yml");
|
|
||||||
const encodedContent = encodeBase64(composeFile);
|
|
||||||
const bashCommand = `
|
|
||||||
rm -rf ${outputPath};
|
|
||||||
mkdir -p ${outputPath};
|
|
||||||
echo "${encodedContent}" | base64 -d > "${filePath}";
|
|
||||||
echo "File 'docker-compose.yml' created: ✅" >> ${logPath};
|
|
||||||
`;
|
|
||||||
return bashCommand;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createComposeFileRaw = async (compose: Compose) => {
|
|
||||||
const { COMPOSE_PATH } = paths();
|
|
||||||
const { appName, composeFile } = compose;
|
|
||||||
const outputPath = join(COMPOSE_PATH, appName, "code");
|
|
||||||
const filePath = join(outputPath, "docker-compose.yml");
|
|
||||||
try {
|
|
||||||
await recreateDirectory(outputPath);
|
|
||||||
await writeFile(filePath, composeFile);
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createComposeFileRawRemote = async (compose: Compose) => {
|
|
||||||
const { COMPOSE_PATH } = paths(true);
|
|
||||||
const { appName, composeFile, serverId } = compose;
|
|
||||||
const outputPath = join(COMPOSE_PATH, appName, "code");
|
|
||||||
const filePath = join(outputPath, "docker-compose.yml");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const encodedContent = encodeBase64(composeFile);
|
|
||||||
const command = `
|
|
||||||
mkdir -p ${outputPath};
|
|
||||||
echo "${encodedContent}" | base64 -d > "${filePath}";
|
|
||||||
`;
|
|
||||||
await execAsyncRemote(serverId, command);
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { findServerById } from "@/server/api/services/server";
|
import { findServerById } from "@dokploy/builders";
|
||||||
import { docker } from "@/server/constants";
|
import { docker } from "@/server/constants";
|
||||||
import Dockerode from "dockerode";
|
import Dockerode from "dockerode";
|
||||||
import { readSSHKey } from "../filesystem/ssh";
|
import { readSSHKey } from "../filesystem/ssh";
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type http from "node:http";
|
|||||||
import { spawn } from "node-pty";
|
import { spawn } from "node-pty";
|
||||||
import { Client } from "ssh2";
|
import { Client } from "ssh2";
|
||||||
import { WebSocketServer } from "ws";
|
import { WebSocketServer } from "ws";
|
||||||
import { findServerById } from "../api/services/server";
|
import { findServerById } from "@dokploy/builders";
|
||||||
import { validateWebSocketRequest } from "../auth/auth";
|
import { validateWebSocketRequest } from "../auth/auth";
|
||||||
import { readSSHKey } from "../utils/filesystem/ssh";
|
import { readSSHKey } from "../utils/filesystem/ssh";
|
||||||
import { getShell } from "./utils";
|
import { getShell } from "./utils";
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type http from "node:http";
|
|||||||
import { spawn } from "node-pty";
|
import { spawn } from "node-pty";
|
||||||
import { Client } from "ssh2";
|
import { Client } from "ssh2";
|
||||||
import { WebSocketServer } from "ws";
|
import { WebSocketServer } from "ws";
|
||||||
import { findServerById } from "../api/services/server";
|
import { findServerById } from "@dokploy/builders";
|
||||||
import { validateWebSocketRequest } from "../auth/auth";
|
import { validateWebSocketRequest } from "../auth/auth";
|
||||||
import { readSSHKey } from "../utils/filesystem/ssh";
|
import { readSSHKey } from "../utils/filesystem/ssh";
|
||||||
import { getShell } from "./utils";
|
import { getShell } from "./utils";
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
|
|||||||
import type http from "node:http";
|
import type http from "node:http";
|
||||||
import { Client } from "ssh2";
|
import { Client } from "ssh2";
|
||||||
import { WebSocketServer } from "ws";
|
import { WebSocketServer } from "ws";
|
||||||
import { findServerById } from "../api/services/server";
|
import { findServerById } from "@dokploy/builders";
|
||||||
import { validateWebSocketRequest } from "../auth/auth";
|
import { validateWebSocketRequest } from "../auth/auth";
|
||||||
import { readSSHKey } from "../utils/filesystem/ssh";
|
import { readSSHKey } from "../utils/filesystem/ssh";
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import path from "node:path";
|
|||||||
import { spawn } from "node-pty";
|
import { spawn } from "node-pty";
|
||||||
import { publicIpv4, publicIpv6 } from "public-ip";
|
import { publicIpv4, publicIpv6 } from "public-ip";
|
||||||
import { WebSocketServer } from "ws";
|
import { WebSocketServer } from "ws";
|
||||||
import { findServerById } from "../api/services/server";
|
import { findServerById } from "@dokploy/builders";
|
||||||
import { validateWebSocketRequest } from "../auth/auth";
|
import { validateWebSocketRequest } from "../auth/auth";
|
||||||
import { paths } from "../constants";
|
import { paths } from "../constants";
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user