Merge branch 'Dokploy:canary' into add-disable-recurse-submodules-option

This commit is contained in:
Yusoof Moh
2025-03-30 12:02:06 +07:00
committed by GitHub
128 changed files with 21061 additions and 1888 deletions

View File

@@ -1,8 +1,8 @@
import { paths } from "@dokploy/server/constants";
import { execAsync } from "../process/execAsync";
import { findAdmin } from "@dokploy/server/services/admin";
import { updateUser } from "@dokploy/server/services/user";
import { scheduleJob, scheduledJobs } from "node-schedule";
import { execAsync } from "../process/execAsync";
const LOG_CLEANUP_JOB_NAME = "access-log-cleanup";

View File

@@ -2,19 +2,19 @@ import path from "node:path";
import { getAllServers } from "@dokploy/server/services/server";
import { scheduleJob } from "node-schedule";
import { db } from "../../db/index";
import { findAdmin } from "../../services/admin";
import {
cleanUpDockerBuilder,
cleanUpSystemPrune,
cleanUpUnusedImages,
} from "../docker/utils";
import { sendDockerCleanupNotifications } from "../notifications/docker-cleanup";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { runMariadbBackup } from "./mariadb";
import { runMongoBackup } from "./mongo";
import { runMySqlBackup } from "./mysql";
import { runPostgresBackup } from "./postgres";
import { findAdmin } from "../../services/admin";
import { getS3Credentials } from "./utils";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import type { BackupSchedule } from "@dokploy/server/services/backup";
import { startLogCleanup } from "../access-log/handler";

View File

@@ -1,11 +1,11 @@
import type { BackupSchedule } from "@dokploy/server/services/backup";
import type { Destination } from "@dokploy/server/services/destination";
import { scheduleJob, scheduledJobs } from "node-schedule";
import { keepLatestNBackups } from ".";
import { runMariadbBackup } from "./mariadb";
import { runMongoBackup } from "./mongo";
import { runMySqlBackup } from "./mysql";
import { runPostgresBackup } from "./postgres";
import { keepLatestNBackups } from ".";
export const scheduleBackup = (backup: BackupSchedule) => {
const { schedule, backupId, databaseType, postgres, mysql, mongo, mariadb } =

View File

@@ -195,6 +195,7 @@ export const mechanizeDockerContainer = async (
try {
const service = docker.getService(appName);
const inspect = await service.inspect();
await service.update({
version: Number.parseInt(inspect.Version.Index),
...settings,
@@ -203,7 +204,7 @@ export const mechanizeDockerContainer = async (
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
},
});
} catch (_error) {
} catch (_error: unknown) {
await docker.createService(settings);
}
};

View File

@@ -1,11 +1,11 @@
import { createHash } from "node:crypto";
import type { WriteStream } from "node:fs";
import { nanoid } from "nanoid";
import type { ApplicationNested } from ".";
import { prepareEnvironmentVariables } from "../docker/utils";
import { getBuildAppDirectory } from "../filesystem/directory";
import { spawnAsync } from "../process/spawnAsync";
import { execAsync } from "../process/execAsync";
import { nanoid } from "nanoid";
import { createHash } from "node:crypto";
import { spawnAsync } from "../process/spawnAsync";
const calculateSecretsHash = (envVariables: string[]): string => {
const hash = createHash("sha256");

View File

@@ -1,20 +1,20 @@
import { deployPostgres } from "@dokploy/server/services/postgres";
import { execAsyncRemote } from "../process/execAsync";
import { execAsync } from "../process/execAsync";
import { deployMySql } from "@dokploy/server/services/mysql";
import { deployMariadb } from "@dokploy/server/services/mariadb";
import { deployMongo } from "@dokploy/server/services/mongo";
import { deployRedis } from "@dokploy/server/services/redis";
import { removeService } from "../docker/utils";
import { db } from "@dokploy/server/db";
import {
postgres,
mysql,
mariadb,
mongo,
mysql,
postgres,
redis,
} from "@dokploy/server/db/schema";
import { deployMariadb } from "@dokploy/server/services/mariadb";
import { deployMongo } from "@dokploy/server/services/mongo";
import { deployMySql } from "@dokploy/server/services/mysql";
import { deployPostgres } from "@dokploy/server/services/postgres";
import { deployRedis } from "@dokploy/server/services/redis";
import { eq } from "drizzle-orm";
import { removeService } from "../docker/utils";
import { execAsyncRemote } from "../process/execAsync";
import { execAsync } from "../process/execAsync";
type DatabaseType = "postgres" | "mysql" | "mariadb" | "mongo" | "redis";

View File

@@ -14,6 +14,10 @@ import {
cloneGitRawRepository,
cloneRawGitRepositoryRemote,
} from "../providers/git";
import {
cloneRawGiteaRepository,
cloneRawGiteaRepositoryRemote,
} from "../providers/gitea";
import {
cloneRawGithubRepository,
cloneRawGithubRepositoryRemote,
@@ -44,6 +48,8 @@ export const cloneCompose = async (compose: Compose) => {
await cloneRawBitbucketRepository(compose);
} else if (compose.sourceType === "git") {
await cloneGitRawRepository(compose);
} else if (compose.sourceType === "gitea") {
await cloneRawGiteaRepository(compose);
} else if (compose.sourceType === "raw") {
await createComposeFileRaw(compose);
}
@@ -58,6 +64,8 @@ export const cloneComposeRemote = async (compose: Compose) => {
await cloneRawBitbucketRepositoryRemote(compose);
} else if (compose.sourceType === "git") {
await cloneRawGitRepositoryRemote(compose);
} else if (compose.sourceType === "gitea") {
await cloneRawGiteaRepositoryRemote(compose);
} else if (compose.sourceType === "raw") {
await createComposeFileRawRemote(compose);
}

View File

@@ -113,6 +113,8 @@ export const getBuildAppDirectory = (application: Application) => {
buildPath = application?.gitlabBuildPath || "";
} else if (sourceType === "bitbucket") {
buildPath = application?.bitbucketBuildPath || "";
} else if (sourceType === "gitea") {
buildPath = application?.giteaBuildPath || "";
} else if (sourceType === "drop") {
buildPath = application?.dropBuildPath || "";
} else if (sourceType === "git") {

View File

@@ -0,0 +1,457 @@
import { createWriteStream } from "node:fs";
import { join } from "node:path";
import { paths } from "@dokploy/server/constants";
import type { Compose } from "@dokploy/server/services/compose";
import {
type Gitea,
findGiteaById,
updateGitea,
} from "@dokploy/server/services/gitea";
import type { InferResultType } from "@dokploy/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 getErrorCloneRequirements = (entity: {
giteaRepository?: string | null;
giteaOwner?: string | null;
giteaBranch?: string | null;
}) => {
const reasons: string[] = [];
const { giteaBranch, giteaOwner, giteaRepository } = entity;
if (!giteaRepository) reasons.push("1. Repository not assigned.");
if (!giteaOwner) reasons.push("2. Owner not specified.");
if (!giteaBranch) reasons.push("3. Branch not defined.");
return reasons;
};
export const refreshGiteaToken = async (giteaProviderId: string) => {
try {
const giteaProvider = await findGiteaById(giteaProviderId);
if (
!giteaProvider?.clientId ||
!giteaProvider?.clientSecret ||
!giteaProvider?.refreshToken
) {
return giteaProvider?.accessToken || null;
}
// Check if token is still valid (add some buffer time, e.g., 5 minutes)
const currentTimeSeconds = Math.floor(Date.now() / 1000);
const bufferTimeSeconds = 300; // 5 minutes
if (
giteaProvider.expiresAt &&
giteaProvider.expiresAt > currentTimeSeconds + bufferTimeSeconds &&
giteaProvider.accessToken
) {
// Token is still valid, no need to refresh
return giteaProvider.accessToken;
}
// Token is expired or about to expire, refresh it
const tokenEndpoint = `${giteaProvider.giteaUrl}/login/oauth/access_token`;
const params = new URLSearchParams({
grant_type: "refresh_token",
refresh_token: giteaProvider.refreshToken,
client_id: giteaProvider.clientId,
client_secret: giteaProvider.clientSecret,
});
const response = await fetch(tokenEndpoint, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
body: params.toString(),
});
if (!response.ok) {
return giteaProvider?.accessToken || null;
}
const data = await response.json();
const { access_token, refresh_token, expires_in } = data;
if (!access_token) {
return giteaProvider?.accessToken || null;
}
const expiresAt = Date.now() + (expires_in || 3600) * 1000;
const expiresAtSeconds = Math.floor(expiresAt / 1000);
await updateGitea(giteaProviderId, {
accessToken: access_token,
refreshToken: refresh_token || giteaProvider.refreshToken,
expiresAt: expiresAtSeconds,
});
return access_token;
} catch (error) {
console.error("Error refreshing Gitea token:", error);
const giteaProvider = await findGiteaById(giteaProviderId);
return giteaProvider?.accessToken || null;
}
};
export type ApplicationWithGitea = InferResultType<
"applications",
{ gitea: true }
>;
export type ComposeWithGitea = InferResultType<"compose", { gitea: true }>;
export const getGiteaCloneCommand = async (
entity: ApplicationWithGitea | ComposeWithGitea,
logPath: string,
isCompose = false,
) => {
const {
appName,
giteaBranch,
giteaId,
giteaOwner,
giteaRepository,
serverId,
gitea,
} = entity;
if (!serverId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Server not found",
});
}
if (!giteaId) {
const command = `
echo "Error: ❌ Gitlab Provider not found" >> ${logPath};
exit 1;
`;
await execAsyncRemote(serverId, command);
throw new TRPCError({
code: "NOT_FOUND",
message: "Gitea Provider not found",
});
}
// Use paths(true) for remote operations
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(true);
await refreshGiteaToken(giteaId);
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
const outputPath = join(basePath, appName, "code");
const baseUrl = gitea?.giteaUrl.replace(/^https?:\/\//, "");
const repoClone = `${giteaOwner}/${giteaRepository}.git`;
const cloneUrl = `https://oauth2:${gitea?.accessToken}@${baseUrl}/${repoClone}`;
const cloneCommand = `
rm -rf ${outputPath};
mkdir -p ${outputPath};
if ! git clone --branch ${giteaBranch} --depth 1 --recurse-submodules ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
echo "❌ [ERROR] Failed to clone the repository ${repoClone}" >> ${logPath};
exit 1;
fi
echo "Cloned ${repoClone} to ${outputPath}: ✅" >> ${logPath};
`;
return cloneCommand;
};
export const cloneGiteaRepository = async (
entity: ApplicationWithGitea | ComposeWithGitea,
logPath: string,
isCompose = false,
) => {
const { APPLICATIONS_PATH, COMPOSE_PATH } = paths();
const writeStream = createWriteStream(logPath, { flags: "a" });
const { appName, giteaBranch, giteaId, giteaOwner, giteaRepository } = entity;
if (!giteaId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Gitea Provider not found",
});
}
await refreshGiteaToken(giteaId);
const giteaProvider = await findGiteaById(giteaId);
if (!giteaProvider) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Gitea provider not found in the database",
});
}
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
const outputPath = join(basePath, appName, "code");
await recreateDirectory(outputPath);
const repoClone = `${giteaOwner}/${giteaRepository}.git`;
const baseUrl = giteaProvider.giteaUrl.replace(/^https?:\/\//, "");
const cloneUrl = `https://oauth2:${giteaProvider.accessToken}@${baseUrl}/${repoClone}`;
writeStream.write(`\nCloning Repo ${repoClone} to ${outputPath}...\n`);
try {
await spawnAsync(
"git",
[
"clone",
"--branch",
giteaBranch!,
"--depth",
"1",
"--recurse-submodules",
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 cloneRawGiteaRepository = async (entity: Compose) => {
const { appName, giteaRepository, giteaOwner, giteaBranch, giteaId } = entity;
const { COMPOSE_PATH } = paths();
if (!giteaId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Gitea Provider not found",
});
}
await refreshGiteaToken(giteaId);
const giteaProvider = await findGiteaById(giteaId);
if (!giteaProvider) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Gitea provider not found in the database",
});
}
const basePath = COMPOSE_PATH;
const outputPath = join(basePath, appName, "code");
await recreateDirectory(outputPath);
const repoClone = `${giteaOwner}/${giteaRepository}.git`;
const baseUrl = giteaProvider.giteaUrl.replace(/^https?:\/\//, "");
const cloneUrl = `https://oauth2:${giteaProvider.accessToken}@${baseUrl}/${repoClone}`;
try {
await spawnAsync("git", [
"clone",
"--branch",
giteaBranch!,
"--depth",
"1",
"--recurse-submodules",
cloneUrl,
outputPath,
"--progress",
]);
} catch (error) {
throw error;
}
};
export const cloneRawGiteaRepositoryRemote = async (compose: Compose) => {
const {
appName,
giteaRepository,
giteaOwner,
giteaBranch,
giteaId,
serverId,
} = compose;
if (!serverId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Server not found",
});
}
if (!giteaId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Gitea Provider not found",
});
}
const { COMPOSE_PATH } = paths(true);
const giteaProvider = await findGiteaById(giteaId);
const basePath = COMPOSE_PATH;
const outputPath = join(basePath, appName, "code");
const repoClone = `${giteaOwner}/${giteaRepository}.git`;
const baseUrl = giteaProvider.giteaUrl.replace(/^https?:\/\//, "");
const cloneUrl = `https://oauth2:${giteaProvider.accessToken}@${baseUrl}/${repoClone}`;
try {
const command = `
rm -rf ${outputPath};
git clone --branch ${giteaBranch} --depth 1 ${cloneUrl} ${outputPath}
`;
await execAsyncRemote(serverId, command);
} catch (error) {
throw error;
}
};
export const haveGiteaRequirements = (giteaProvider: Gitea) => {
return !!(giteaProvider?.clientId && giteaProvider?.clientSecret);
};
export const testGiteaConnection = async (input: { giteaId: string }) => {
try {
const { giteaId } = input;
if (!giteaId) {
throw new Error("Gitea provider not found");
}
const giteaProvider = await findGiteaById(giteaId);
if (!giteaProvider) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Gitea provider not found in the database",
});
}
await refreshGiteaToken(giteaId);
const provider = await findGiteaById(giteaId);
if (!provider || !provider.accessToken) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "No access token available. Please authorize with Gitea.",
});
}
const baseUrl = provider.giteaUrl.replace(/\/+$/, "");
const url = `${baseUrl}/api/v1/user/repos`;
const response = await fetch(url, {
headers: {
Accept: "application/json",
Authorization: `token ${provider.accessToken}`,
},
});
if (!response.ok) {
throw new Error(
`Failed to connect to Gitea API: ${response.status} ${response.statusText}`,
);
}
const repos = await response.json();
await updateGitea(giteaId, {
lastAuthenticatedAt: Math.floor(Date.now() / 1000),
});
return repos.length;
} catch (error) {
throw error;
}
};
export const getGiteaRepositories = async (giteaId?: string) => {
if (!giteaId) {
return [];
}
await refreshGiteaToken(giteaId);
const giteaProvider = await findGiteaById(giteaId);
const baseUrl = giteaProvider.giteaUrl.replace(/\/+$/, "");
const url = `${baseUrl}/api/v1/user/repos`;
const response = await fetch(url, {
headers: {
Accept: "application/json",
Authorization: `token ${giteaProvider.accessToken}`,
},
});
if (!response.ok) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `Failed to fetch repositories: ${response.statusText}`,
});
}
const repositories = await response.json();
const mappedRepositories = repositories.map((repo: any) => ({
id: repo.id,
name: repo.name,
url: repo.full_name,
owner: {
username: repo.owner.login,
},
}));
return mappedRepositories;
};
export const getGiteaBranches = async (input: {
giteaId?: string;
owner: string;
repo: string;
}) => {
if (!input.giteaId) {
return [];
}
await refreshGiteaToken(input.giteaId);
const giteaProvider = await findGiteaById(input.giteaId);
const baseUrl = giteaProvider.giteaUrl.replace(/\/+$/, "");
const url = `${baseUrl}/api/v1/repos/${input.owner}/${input.repo}/branches`;
const response = await fetch(url, {
headers: {
Accept: "application/json",
Authorization: `token ${giteaProvider.accessToken}`,
},
});
if (!response.ok) {
throw new Error(`Failed to fetch branches: ${response.statusText}`);
}
const branches = await response.json();
return branches.map((branch: any) => ({
id: branch.name,
name: branch.name,
commit: {
id: branch.commit.id,
},
})) as {
id: string;
name: string;
commit: {
id: string;
};
}[];
};

View File

@@ -1,11 +1,11 @@
import type { Mariadb } from "@dokploy/server/services/mariadb";
import type { Destination } from "@dokploy/server/services/destination";
import type { Mariadb } from "@dokploy/server/services/mariadb";
import { getS3Credentials } from "../backups/utils";
import {
getRemoteServiceContainer,
getServiceContainer,
} from "../docker/utils";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getS3Credentials } from "../backups/utils";
export const restoreMariadbBackup = async (
mariadb: Mariadb,

View File

@@ -1,11 +1,11 @@
import type { Mongo } from "@dokploy/server/services/mongo";
import type { Destination } from "@dokploy/server/services/destination";
import type { Mongo } from "@dokploy/server/services/mongo";
import { getS3Credentials } from "../backups/utils";
import {
getRemoteServiceContainer,
getServiceContainer,
} from "../docker/utils";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getS3Credentials } from "../backups/utils";
export const restoreMongoBackup = async (
mongo: Mongo,

View File

@@ -1,11 +1,11 @@
import type { MySql } from "@dokploy/server/services/mysql";
import type { Destination } from "@dokploy/server/services/destination";
import type { MySql } from "@dokploy/server/services/mysql";
import { getS3Credentials } from "../backups/utils";
import {
getRemoteServiceContainer,
getServiceContainer,
} from "../docker/utils";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getS3Credentials } from "../backups/utils";
export const restoreMySqlBackup = async (
mysql: MySql,

View File

@@ -1,11 +1,11 @@
import type { Postgres } from "@dokploy/server/services/postgres";
import type { Destination } from "@dokploy/server/services/destination";
import type { Postgres } from "@dokploy/server/services/postgres";
import { getS3Credentials } from "../backups/utils";
import {
getRemoteServiceContainer,
getServiceContainer,
} from "../docker/utils";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getS3Credentials } from "../backups/utils";
export const restorePostgresBackup = async (
postgres: Postgres,