Merge branch 'canary' into 319-ability-to-roll-back-service-depoyments

This commit is contained in:
Mauricio Siu
2025-06-01 19:44:02 -06:00
109 changed files with 9421 additions and 4008 deletions

View File

@@ -28,6 +28,8 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"pino": "9.4.0",
"pino-pretty": "11.2.2",
"micromatch": "4.0.8",
"@ai-sdk/anthropic": "^1.0.6",
"@ai-sdk/azure": "^1.0.15",

View File

@@ -208,6 +208,7 @@ export const applications = pgTable("application", {
buildType: buildType("buildType").notNull().default("nixpacks"),
herokuVersion: text("herokuVersion").default("24"),
publishDirectory: text("publishDirectory"),
isStaticSpa: boolean("isStaticSpa"),
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
@@ -412,6 +413,7 @@ const createSchema = createInsertSchema(applications, {
]),
herokuVersion: z.string().optional(),
publishDirectory: z.string().optional(),
isStaticSpa: z.boolean().optional(),
owner: z.string(),
healthCheckSwarm: HealthCheckSwarmSchema.nullable(),
restartPolicySwarm: RestartPolicySwarmSchema.nullable(),
@@ -464,7 +466,7 @@ export const apiSaveBuildType = createSchema
herokuVersion: true,
})
.required()
.merge(createSchema.pick({ publishDirectory: true }));
.merge(createSchema.pick({ publishDirectory: true, isStaticSpa: true }));
export const apiSaveGithubProvider = createSchema
.pick({

View File

@@ -11,15 +11,15 @@ import {
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { generateAppName } from ".";
import { compose } from "./compose";
import { deployments } from "./deployment";
import { destinations } from "./destination";
import { mariadb } from "./mariadb";
import { mongo } from "./mongo";
import { mysql } from "./mysql";
import { postgres } from "./postgres";
import { users_temp } from "./user";
import { compose } from "./compose";
import { deployments } from "./deployment";
import { generateAppName } from ".";
export const databaseType = pgEnum("databaseType", [
"postgres",
"mariadb",

View File

@@ -3,6 +3,7 @@ import { boolean, integer, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { backups } from "./backups";
import { bitbucket } from "./bitbucket";
import { deployments } from "./deployment";
import { domains } from "./domain";
@@ -15,7 +16,6 @@ import { server } from "./server";
import { applicationStatus, triggerType } from "./shared";
import { sshKeys } from "./ssh-key";
import { generateAppName } from "./utils";
import { backups } from "./backups";
import { schedules } from "./schedule";
export const sourceTypeCompose = pgEnum("sourceTypeCompose", [

View File

@@ -10,11 +10,11 @@ import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { applications } from "./application";
import { backups } from "./backups";
import { compose } from "./compose";
import { previewDeployments } from "./preview-deployments";
import { server } from "./server";
import { schedules } from "./schedule";
import { backups } from "./backups";
import { server } from "./server";
export const deploymentStatus = pgEnum("deploymentStatus", [
"running",
"done",

View File

@@ -4,11 +4,11 @@ import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { applications } from "./application";
import { deployments } from "./deployment";
import { generateAppName } from "./utils";
import { compose } from "./compose";
import { deployments } from "./deployment";
import { server } from "./server";
import { users_temp } from "./user";
import { generateAppName } from "./utils";
export const shellTypes = pgEnum("shellType", ["bash", "sh"]);
export const scheduleType = pgEnum("scheduleType", [

View File

@@ -20,9 +20,9 @@ import { mongo } from "./mongo";
import { mysql } from "./mysql";
import { postgres } from "./postgres";
import { redis } from "./redis";
import { schedules } from "./schedule";
import { sshKeys } from "./ssh-key";
import { generateAppName } from "./utils";
import { schedules } from "./schedule";
export const serverStatus = pgEnum("serverStatus", ["active", "inactive"]);
export const server = pgTable("server", {

View File

@@ -11,10 +11,10 @@ import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { account, apikey, organization } from "./account";
import { projects } from "./project";
import { certificateType } from "./shared";
import { backups } from "./backups";
import { projects } from "./project";
import { schedules } from "./schedule";
import { certificateType } from "./shared";
/**
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
* database instance for multiple projects.

View File

@@ -132,3 +132,5 @@ export {
export * from "./utils/schedules/utils";
export * from "./utils/schedules/index";
export * from "./lib/logger";

View File

@@ -3,7 +3,7 @@ import * as bcrypt from "bcrypt";
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { APIError } from "better-auth/api";
import { apiKey, organization, twoFactor, admin } from "better-auth/plugins";
import { admin, apiKey, organization, twoFactor } from "better-auth/plugins";
import { and, desc, eq } from "drizzle-orm";
import { IS_CLOUD } from "../constants";
import { db } from "../db";

View File

@@ -0,0 +1,11 @@
import pino from "pino";
export const logger = pino({
transport: {
target: "pino-pretty",
options: {
colorize: true,
levelFirst: false,
},
},
});

View File

@@ -24,13 +24,13 @@ import { type Compose, findComposeById, updateCompose } from "./compose";
import { type Server, findServerById } from "./server";
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
import { findBackupById } from "./backup";
import {
type PreviewDeployment,
findPreviewDeploymentById,
updatePreviewDeployment,
} from "./preview-deployment";
import { findScheduleById } from "./schedule";
import { findBackupById } from "./backup";
export type Deployment = typeof deployments.$inferSelect;

View File

@@ -1,3 +1,5 @@
import dns from "node:dns";
import { promisify } from "node:util";
import { db } from "@dokploy/server/db";
import { generateRandomDomain } from "@dokploy/server/templates";
import { manageDomain } from "@dokploy/server/utils/traefik/domain";
@@ -7,8 +9,6 @@ import { type apiCreateDomain, domains } from "../db/schema";
import { findUserById } from "./admin";
import { findApplicationById } from "./application";
import { findServerById } from "./server";
import dns from "node:dns";
import { promisify } from "node:util";
export type Domain = typeof domains.$inferSelect;

View File

@@ -1,16 +1,16 @@
import { type Schedule, schedules } from "../db/schema/schedule";
import { db } from "../db";
import { eq } from "drizzle-orm";
import path from "node:path";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import type { z } from "zod";
import { paths } from "../constants";
import { db } from "../db";
import { type Schedule, schedules } from "../db/schema/schedule";
import type {
createScheduleSchema,
updateScheduleSchema,
} from "../db/schema/schedule";
import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
import { paths } from "../constants";
import path from "node:path";
import { encodeBase64 } from "../utils/docker/utils";
import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
export type ScheduleExtended = Awaited<ReturnType<typeof findScheduleById>>;

View File

@@ -356,20 +356,20 @@ const installUtilities = () => `
case "$OS_TYPE" in
arch)
pacman -Sy --noconfirm --needed curl wget git jq openssl >/dev/null || true
pacman -Sy --noconfirm --needed curl wget git git-lfs jq openssl >/dev/null || true
;;
alpine)
sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories
apk update >/dev/null
apk add curl wget git jq openssl sudo unzip tar >/dev/null
apk add curl wget git git-lfs jq openssl sudo unzip tar >/dev/null
;;
ubuntu | debian | raspbian)
DEBIAN_FRONTEND=noninteractive apt-get update -y >/dev/null
DEBIAN_FRONTEND=noninteractive apt-get install -y unzip curl wget git jq openssl >/dev/null
DEBIAN_FRONTEND=noninteractive apt-get install -y unzip curl wget git git-lfs jq openssl >/dev/null
;;
centos | fedora | rhel | ol | rocky | almalinux | amzn)
if [ "$OS_TYPE" = "amzn" ]; then
dnf install -y wget git jq openssl >/dev/null
dnf install -y wget git git-lfs jq openssl >/dev/null
else
if ! command -v dnf >/dev/null; then
yum install -y dnf >/dev/null
@@ -377,12 +377,12 @@ const installUtilities = () => `
if ! command -v curl >/dev/null; then
dnf install -y curl >/dev/null
fi
dnf install -y wget git jq openssl unzip >/dev/null
dnf install -y wget git git-lfs jq openssl unzip >/dev/null
fi
;;
sles | opensuse-leap | opensuse-tumbleweed)
zypper refresh >/dev/null
zypper install -y curl wget git jq openssl >/dev/null
zypper install -y curl wget git git-lfs jq openssl >/dev/null
;;
*)
echo "This script only supports Debian, Redhat, Arch Linux, or SLES based operating systems for now."
@@ -577,7 +577,7 @@ const installNixpacks = () => `
if command_exists nixpacks; then
echo "Nixpacks already installed ✅"
else
export NIXPACKS_VERSION=1.35.0
export NIXPACKS_VERSION=1.39.0
bash -c "$(curl -fsSL https://nixpacks.com/install.sh)"
echo "Nixpacks version $NIXPACKS_VERSION installed ✅"
fi

View File

@@ -139,6 +139,8 @@ export const initializeTraefik = async ({
const newContainer = docker.getContainer(containerName);
await newContainer.start();
console.log("Traefik container started successfully after retry");
} else {
throw error;
}
}
} catch (error) {

View File

@@ -1,4 +1,4 @@
import { randomBytes, createHmac } from "node:crypto";
import { createHmac, randomBytes } from "node:crypto";
import { existsSync } from "node:fs";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
@@ -35,7 +35,7 @@ export const generateRandomDomain = ({
projectName,
}: Schema): string => {
const hash = randomBytes(3).toString("hex");
const slugIp = serverIp.replaceAll(".", "-");
const slugIp = serverIp.replaceAll(".", "-").replaceAll(":", "-");
return `${projectName}-${hash}${slugIp === "" ? "" : `-${slugIp}`}.traefik.me`;
};

View File

@@ -1,13 +1,13 @@
import type { BackupSchedule } from "@dokploy/server/services/backup";
import type { Compose } from "@dokploy/server/services/compose";
import { findProjectById } from "@dokploy/server/services/project";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getS3Credentials, normalizeS3Path, getBackupCommand } from "./utils";
import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import { findProjectById } from "@dokploy/server/services/project";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
export const runComposeBackup = async (
compose: Compose,

View File

@@ -11,10 +11,10 @@ import { sendDockerCleanupNotifications } from "../notifications/docker-cleanup"
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getS3Credentials, scheduleBackup } from "./utils";
import type { BackupSchedule } from "@dokploy/server/services/backup";
import { startLogCleanup } from "../access-log/handler";
import { member } from "@dokploy/server/db/schema";
import type { BackupSchedule } from "@dokploy/server/services/backup";
import { eq } from "drizzle-orm";
import { startLogCleanup } from "../access-log/handler";
export const initCronJobs = async () => {
console.log("Setting up cron jobs....");

View File

@@ -1,13 +1,13 @@
import type { BackupSchedule } from "@dokploy/server/services/backup";
import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import type { Mariadb } from "@dokploy/server/services/mariadb";
import { findProjectById } from "@dokploy/server/services/project";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
export const runMariadbBackup = async (
mariadb: Mariadb,

View File

@@ -1,13 +1,13 @@
import type { BackupSchedule } from "@dokploy/server/services/backup";
import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import type { Mongo } from "@dokploy/server/services/mongo";
import { findProjectById } from "@dokploy/server/services/project";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
const { projectId, name } = mongo;

View File

@@ -1,13 +1,13 @@
import type { BackupSchedule } from "@dokploy/server/services/backup";
import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import type { MySql } from "@dokploy/server/services/mysql";
import { findProjectById } from "@dokploy/server/services/project";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => {
const { projectId, name } = mysql;

View File

@@ -1,13 +1,13 @@
import type { BackupSchedule } from "@dokploy/server/services/backup";
import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import type { Postgres } from "@dokploy/server/services/postgres";
import { findProjectById } from "@dokploy/server/services/project";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
export const runPostgresBackup = async (
postgres: Postgres,

View File

@@ -1,13 +1,14 @@
import { logger } from "@dokploy/server/lib/logger";
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 { runComposeBackup } from "./compose";
import { runMariadbBackup } from "./mariadb";
import { runMongoBackup } from "./mongo";
import { runMySqlBackup } from "./mysql";
import { runPostgresBackup } from "./postgres";
import { runWebServerBackup } from "./web-server";
import { runComposeBackup } from "./compose";
export const scheduleBackup = (backup: BackupSchedule) => {
const {
@@ -222,6 +223,17 @@ export const getBackupCommand = (
) => {
const containerSearch = getContainerSearchCommand(backup);
const backupCommand = generateBackupCommand(backup);
logger.info(
{
containerSearch,
backupCommand,
rcloneCommand,
logPath,
},
`Executing backup command: ${backup.databaseType} ${backup.backupType}`,
);
return `
set -eo pipefail;
echo "[$(date)] Starting backup process..." >> ${logPath};

View File

@@ -1,16 +1,16 @@
import type { BackupSchedule } from "@dokploy/server/services/backup";
import { execAsync } from "../process/execAsync";
import { getS3Credentials, normalizeS3Path } from "./utils";
import { findDestinationById } from "@dokploy/server/services/destination";
import { IS_CLOUD, paths } from "@dokploy/server/constants";
import { mkdtemp } from "node:fs/promises";
import { join } from "node:path";
import { createWriteStream } from "node:fs";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { IS_CLOUD, paths } from "@dokploy/server/constants";
import type { BackupSchedule } from "@dokploy/server/services/backup";
import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import { createWriteStream } from "node:fs";
import { findDestinationById } from "@dokploy/server/services/destination";
import { execAsync } from "../process/execAsync";
import { getS3Credentials, normalizeS3Path } from "./utils";
export const runWebServerBackup = async (backup: BackupSchedule) => {
if (IS_CLOUD) {
@@ -51,13 +51,23 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
const postgresContainerId = containerId.trim();
const postgresCommand = `docker exec ${postgresContainerId} pg_dump -v -Fc -U dokploy -d dokploy > '${tempDir}/database.sql'`;
// First dump the database inside the container
const dumpCommand = `docker exec ${postgresContainerId} pg_dump -v -Fc -U dokploy -d dokploy -f /tmp/database.sql`;
writeStream.write(`Running dump command: ${dumpCommand}\n`);
await execAsync(dumpCommand);
writeStream.write(`Running command: ${postgresCommand}\n`);
await execAsync(postgresCommand);
// Then copy the file from the container to host
const copyCommand = `docker cp ${postgresContainerId}:/tmp/database.sql ${tempDir}/database.sql`;
writeStream.write(`Copying database dump: ${copyCommand}\n`);
await execAsync(copyCommand);
// Clean up the temp file in the container
const cleanupCommand = `docker exec ${postgresContainerId} rm -f /tmp/database.sql`;
writeStream.write(`Cleaning up temp file: ${cleanupCommand}\n`);
await execAsync(cleanupCommand);
await execAsync(
`rsync -av --ignore-errors ${BASE_PATH}/ ${tempDir}/filesystem/`,
`rsync -a --ignore-errors ${BASE_PATH}/ ${tempDir}/filesystem/`,
);
writeStream.write("Copied filesystem to temp directory\n");
@@ -77,7 +87,11 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
await updateDeploymentStatus(deployment.deploymentId, "done");
return true;
} finally {
await execAsync(`rm -rf ${tempDir}`);
try {
await rm(tempDir, { recursive: true, force: true });
} catch (cleanupError) {
console.error("Cleanup error:", cleanupError);
}
}
} catch (error) {
console.error("Backup error:", error);

View File

@@ -190,7 +190,8 @@ const createEnvFile = (compose: ComposeNested) => {
join(COMPOSE_PATH, appName, "code", "docker-compose.yml");
const envFilePath = join(dirname(composeFilePath), ".env");
let envContent = env || "";
let envContent = `APP_NAME=${appName}\n`;
envContent += env || "";
if (!envContent.includes("DOCKER_CONFIG")) {
envContent += "\nDOCKER_CONFIG=/root/.docker/config.json";
}
@@ -219,7 +220,8 @@ export const getCreateEnvFileCommand = (compose: ComposeNested) => {
const envFilePath = join(dirname(composeFilePath), ".env");
let envContent = env || "";
let envContent = `APP_NAME=${appName}\n`;
envContent += env || "";
if (!envContent.includes("DOCKER_CONFIG")) {
envContent += "\nDOCKER_CONFIG=/root/.docker/config.json";
}

View File

@@ -2,7 +2,10 @@ 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 {
parseEnvironmentKeyValuePair,
prepareEnvironmentVariables,
} from "../docker/utils";
import { getBuildAppDirectory } from "../filesystem/directory";
import { execAsync } from "../process/execAsync";
import { spawnAsync } from "../process/spawnAsync";
@@ -81,10 +84,10 @@ export const buildRailpack = async (
// Add secrets properly formatted
const env: { [key: string]: string } = {};
for (const envVar of envVariables) {
const [key, value] = envVar.split("=");
for (const pair of envVariables) {
const [key, value] = parseEnvironmentKeyValuePair(pair);
if (key && value) {
buildArgs.push("--secret", `id=${key},env='${key}'`);
buildArgs.push("--secret", `id=${key},env=${key}`);
env[key] = value;
}
}
@@ -161,11 +164,11 @@ export const getRailpackCommand = (
// Add secrets properly formatted
const exportEnvs = [];
for (const envVar of envVariables) {
const [key, value] = envVar.split("=");
for (const pair of envVariables) {
const [key, value] = parseEnvironmentKeyValuePair(pair);
if (key && value) {
buildArgs.push("--secret", `id=${key},env='${key}'`);
exportEnvs.push(`export ${key}=${value}`);
buildArgs.push("--secret", `id=${key},env=${key}`);
exportEnvs.push(`export ${key}='${value}'`);
}
}

View File

@@ -7,21 +7,58 @@ import type { ApplicationNested } from ".";
import { createFile, getCreateFileCommand } from "../docker/utils";
import { getBuildAppDirectory } from "../filesystem/directory";
const nginxSpaConfig = `
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
access_log /dev/stdout;
error_log /dev/stderr;
server {
listen 80;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
}
}
`;
export const buildStatic = async (
application: ApplicationNested,
writeStream: WriteStream,
) => {
const { publishDirectory } = application;
const { publishDirectory, isStaticSpa } = application;
const buildAppDirectory = getBuildAppDirectory(application);
try {
if (isStaticSpa) {
createFile(buildAppDirectory, "nginx.conf", nginxSpaConfig);
}
createFile(
buildAppDirectory,
".dockerignore",
[".git", ".env", "Dockerfile", ".dockerignore"].join("\n"),
);
createFile(
buildAppDirectory,
"Dockerfile",
[
"FROM nginx:alpine",
"WORKDIR /usr/share/nginx/html/",
isStaticSpa ? "COPY nginx.conf /etc/nginx/nginx.conf" : "",
`COPY ${publishDirectory || "."} .`,
'CMD ["nginx", "-g", "daemon off;"]',
].join("\n"),
);

View File

@@ -2,6 +2,7 @@ import fs from "node:fs";
import path from "node:path";
import type { Readable } from "node:stream";
import { docker, paths } from "@dokploy/server/constants";
import type { Compose } from "@dokploy/server/services/compose";
import type { ContainerInfo, ResourceRequirements } from "dockerode";
import { parse } from "dotenv";
import type { ApplicationNested } from "../builders";
@@ -13,7 +14,6 @@ import type { RedisNested } from "../databases/redis";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { spawnAsync } from "../process/spawnAsync";
import { getRemoteDocker } from "../servers/remote-docker";
import type { Compose } from "@dokploy/server/services/compose";
interface RegistryAuth {
username: string;
@@ -279,6 +279,17 @@ export const prepareEnvironmentVariables = (
return resolvedVars;
};
export const parseEnvironmentKeyValuePair = (
pair: string,
): [string, string] => {
const [key, ...valueParts] = pair.split("=");
if (!key || !valueParts.length) {
throw new Error(`Invalid environment variable pair: ${pair}`);
}
return [key, valueParts.join("")];
};
export const getEnviromentVariablesObject = (
input: string | null,
projectEnv?: string | null,
@@ -288,7 +299,7 @@ export const getEnviromentVariablesObject = (
const jsonObject: Record<string, string> = {};
for (const pair of envs) {
const [key, value] = pair.split("=");
const [key, value] = parseEnvironmentKeyValuePair(pair);
if (key && value) {
jsonObject[key] = value;
}

View File

@@ -246,32 +246,18 @@ export const getGitlabRepositories = async (gitlabId?: string) => {
const gitlabProvider = await findGitlabById(gitlabId);
const response = await fetch(
`${gitlabProvider.gitlabUrl}/api/v4/projects?membership=true&owned=true&page=${0}&per_page=${100}`,
{
headers: {
Authorization: `Bearer ${gitlabProvider.accessToken}`,
},
},
);
const allProjects = await validateGitlabProvider(gitlabProvider);
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 filteredRepos = allProjects.filter((repo: any) => {
const { full_path, kind } = repo.namespace;
const groupName = gitlabProvider.groupName?.toLowerCase();
if (groupName) {
const isIncluded = groupName
.split(",")
.some((name) => full_path.toLowerCase().includes(name));
.some((name) =>
full_path.toLowerCase().startsWith(name.trim().toLowerCase()),
);
return isIncluded && kind === "group";
}
@@ -432,23 +418,7 @@ export const testGitlabConnection = async (
const gitlabProvider = await findGitlabById(gitlabId);
const response = await fetch(
`${gitlabProvider.gitlabUrl}/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 repositories = await validateGitlabProvider(gitlabProvider);
const filteredRepos = repositories.filter((repo: any) => {
const { full_path, kind } = repo.namespace;
@@ -456,10 +426,56 @@ export const testGitlabConnection = async (
if (groupName) {
return groupName
.split(",")
.some((name) => full_path.toLowerCase().includes(name));
.some((name) =>
full_path.toLowerCase().startsWith(name.trim().toLowerCase()),
);
}
return kind === "user";
});
return filteredRepos.length;
};
export const validateGitlabProvider = async (gitlabProvider: Gitlab) => {
try {
const allProjects = [];
let page = 1;
const perPage = 100; // GitLab's max per page is 100
while (true) {
const response = await fetch(
`${gitlabProvider.gitlabUrl}/api/v4/projects?membership=true&owned=true&page=${page}&per_page=${perPage}`,
{
headers: {
Authorization: `Bearer ${gitlabProvider.accessToken}`,
},
},
);
if (!response.ok) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `Failed to fetch repositories: ${response.statusText}`,
});
}
const projects = await response.json();
if (projects.length === 0) {
break;
}
allProjects.push(...projects);
page++;
const total = response.headers.get("x-total");
if (total && allProjects.length >= Number.parseInt(total)) {
break;
}
}
return allProjects;
} catch (error) {
throw error;
}
};

View File

@@ -1,10 +1,10 @@
import type { Destination } from "@dokploy/server/services/destination";
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
import type { Compose } from "@dokploy/server/services/compose";
import type { Destination } from "@dokploy/server/services/destination";
import type { z } from "zod";
import { getS3Credentials } from "../backups/utils";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getRestoreCommand } from "./utils";
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
import type { z } from "zod";
interface DatabaseCredentials {
databaseUser?: string;

View File

@@ -1,10 +1,10 @@
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
import type { Destination } from "@dokploy/server/services/destination";
import type { Mariadb } from "@dokploy/server/services/mariadb";
import type { z } from "zod";
import { getS3Credentials } from "../backups/utils";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getRestoreCommand } from "./utils";
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
import type { z } from "zod";
export const restoreMariadbBackup = async (
mariadb: Mariadb,

View File

@@ -1,10 +1,10 @@
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
import type { Destination } from "@dokploy/server/services/destination";
import type { Mongo } from "@dokploy/server/services/mongo";
import type { z } from "zod";
import { getS3Credentials } from "../backups/utils";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getRestoreCommand } from "./utils";
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
import type { z } from "zod";
export const restoreMongoBackup = async (
mongo: Mongo,

View File

@@ -1,10 +1,10 @@
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
import type { Destination } from "@dokploy/server/services/destination";
import type { MySql } from "@dokploy/server/services/mysql";
import type { z } from "zod";
import { getS3Credentials } from "../backups/utils";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getRestoreCommand } from "./utils";
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
import type { z } from "zod";
export const restoreMySqlBackup = async (
mysql: MySql,

View File

@@ -1,10 +1,10 @@
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
import type { Destination } from "@dokploy/server/services/destination";
import type { Postgres } from "@dokploy/server/services/postgres";
import type { z } from "zod";
import { getS3Credentials } from "../backups/utils";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getRestoreCommand } from "./utils";
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
import type { z } from "zod";
export const restorePostgresBackup = async (
postgres: Postgres,

View File

@@ -1,10 +1,10 @@
import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { IS_CLOUD, paths } from "@dokploy/server/constants";
import type { Destination } from "@dokploy/server/services/destination";
import { getS3Credentials } from "../backups/utils";
import { execAsync } from "../process/execAsync";
import { paths, IS_CLOUD } from "@dokploy/server/constants";
import { mkdtemp } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
export const restoreWebServerBackup = async (
destination: Destination,

View File

@@ -1,6 +1,6 @@
import { db } from "../../db/index";
import { schedules } from "@dokploy/server/db/schema";
import { eq } from "drizzle-orm";
import { db } from "../../db/index";
import { scheduleJob } from "./utils";
export const initSchedules = async () => {

View File

@@ -1,14 +1,14 @@
import { createWriteStream } from "node:fs";
import path from "node:path";
import { paths } from "@dokploy/server/constants";
import type { Schedule } from "@dokploy/server/db/schema/schedule";
import { createDeploymentSchedule } from "@dokploy/server/services/deployment";
import { updateDeploymentStatus } from "@dokploy/server/services/deployment";
import { findScheduleById } from "@dokploy/server/services/schedule";
import { scheduledJobs, scheduleJob as scheduleJobNode } from "node-schedule";
import { scheduleJob as scheduleJobNode, scheduledJobs } from "node-schedule";
import { getComposeContainer, getServiceContainer } from "../docker/utils";
import { execAsyncRemote } from "../process/execAsync";
import { spawnAsync } from "../process/spawnAsync";
import { createDeploymentSchedule } from "@dokploy/server/services/deployment";
import { createWriteStream } from "node:fs";
import { updateDeploymentStatus } from "@dokploy/server/services/deployment";
import { paths } from "@dokploy/server/constants";
import path from "node:path";
export const scheduleJob = (schedule: Schedule) => {
const { cronExpression, scheduleId } = schedule;