mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-04 13:35:22 +02:00
Merge branch 'canary' into ulimits-at-0a401843
This commit is contained in:
@@ -116,3 +116,22 @@ export const getDokployUrl = async () => {
|
||||
}
|
||||
return `http://${settings?.serverIp}:${process.env.PORT}`;
|
||||
};
|
||||
|
||||
export const getTrustedOrigins = async () => {
|
||||
const members = await db.query.member.findMany({
|
||||
where: eq(member.role, "owner"),
|
||||
with: {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (members.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const trustedOrigins = members.flatMap(
|
||||
(member) => member.user.trustedOrigins || [],
|
||||
);
|
||||
|
||||
return Array.from(new Set(trustedOrigins));
|
||||
};
|
||||
|
||||
@@ -174,6 +174,10 @@ export const deployApplication = async ({
|
||||
}) => {
|
||||
const application = await findApplicationById(applicationId);
|
||||
const serverId = application.buildServerId || application.serverId;
|
||||
const applicationEntity = {
|
||||
...application,
|
||||
serverId: serverId,
|
||||
};
|
||||
|
||||
const buildLink = `${await getDokployUrl()}/dashboard/project/${application.environment.projectId}/environment/${application.environmentId}/services/application/${application.applicationId}?tab=deployments`;
|
||||
const deployment = await createDeployment({
|
||||
@@ -185,15 +189,15 @@ export const deployApplication = async ({
|
||||
try {
|
||||
let command = "set -e;";
|
||||
if (application.sourceType === "github") {
|
||||
command += await cloneGithubRepository(application);
|
||||
command += await cloneGithubRepository(applicationEntity);
|
||||
} else if (application.sourceType === "gitlab") {
|
||||
command += await cloneGitlabRepository(application);
|
||||
command += await cloneGitlabRepository(applicationEntity);
|
||||
} else if (application.sourceType === "gitea") {
|
||||
command += await cloneGiteaRepository(application);
|
||||
command += await cloneGiteaRepository(applicationEntity);
|
||||
} else if (application.sourceType === "bitbucket") {
|
||||
command += await cloneBitbucketRepository(application);
|
||||
command += await cloneBitbucketRepository(applicationEntity);
|
||||
} else if (application.sourceType === "git") {
|
||||
command += await cloneGitRepository(application);
|
||||
command += await cloneGitRepository(applicationEntity);
|
||||
} else if (application.sourceType === "docker") {
|
||||
command += await buildRemoteDocker(application);
|
||||
}
|
||||
@@ -253,8 +257,11 @@ export const deployApplication = async ({
|
||||
} finally {
|
||||
// Only extract commit info for non-docker sources
|
||||
if (application.sourceType !== "docker") {
|
||||
const commitInfo = await getGitCommitInfo(application);
|
||||
|
||||
const commitInfo = await getGitCommitInfo({
|
||||
appName: application.appName,
|
||||
type: "application",
|
||||
serverId: serverId,
|
||||
});
|
||||
if (commitInfo) {
|
||||
await updateDeployment(deployment.deploymentId, {
|
||||
title: commitInfo.message,
|
||||
@@ -452,6 +459,137 @@ export const deployPreviewApplication = async ({
|
||||
return true;
|
||||
};
|
||||
|
||||
export const rebuildPreviewApplication = async ({
|
||||
applicationId,
|
||||
titleLog = "Rebuild Preview Deployment",
|
||||
descriptionLog = "",
|
||||
previewDeploymentId,
|
||||
}: {
|
||||
applicationId: string;
|
||||
titleLog: string;
|
||||
descriptionLog: string;
|
||||
previewDeploymentId: string;
|
||||
}) => {
|
||||
const application = await findApplicationById(applicationId);
|
||||
const previewDeployment =
|
||||
await findPreviewDeploymentById(previewDeploymentId);
|
||||
|
||||
const deployment = await createDeploymentPreview({
|
||||
title: titleLog,
|
||||
description: descriptionLog,
|
||||
previewDeploymentId: previewDeploymentId,
|
||||
});
|
||||
|
||||
const previewDomain = getDomainHost(previewDeployment?.domain as Domain);
|
||||
const issueParams = {
|
||||
owner: application?.owner || "",
|
||||
repository: application?.repository || "",
|
||||
issue_number: previewDeployment.pullRequestNumber,
|
||||
comment_id: Number.parseInt(previewDeployment.pullRequestCommentId),
|
||||
githubId: application?.githubId || "",
|
||||
};
|
||||
|
||||
try {
|
||||
const commentExists = await issueCommentExists({
|
||||
...issueParams,
|
||||
});
|
||||
if (!commentExists) {
|
||||
const result = await createPreviewDeploymentComment({
|
||||
...issueParams,
|
||||
previewDomain,
|
||||
appName: previewDeployment.appName,
|
||||
githubId: application?.githubId || "",
|
||||
previewDeploymentId,
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Pull request comment not found",
|
||||
});
|
||||
}
|
||||
|
||||
issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId);
|
||||
}
|
||||
|
||||
const buildingComment = getIssueComment(
|
||||
application.name,
|
||||
"running",
|
||||
previewDomain,
|
||||
);
|
||||
await updateIssueComment({
|
||||
...issueParams,
|
||||
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
|
||||
});
|
||||
|
||||
// Set application properties for preview deployment
|
||||
application.appName = previewDeployment.appName;
|
||||
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
||||
application.buildArgs = `${application.previewBuildArgs}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
||||
application.buildSecrets = `${application.previewBuildSecrets}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
||||
application.rollbackActive = false;
|
||||
application.buildRegistry = null;
|
||||
application.rollbackRegistry = null;
|
||||
application.registry = null;
|
||||
|
||||
const serverId = application.serverId;
|
||||
let command = "set -e;";
|
||||
// Only rebuild, don't clone repository
|
||||
command += await getBuildCommand(application);
|
||||
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, commandWithLog);
|
||||
} else {
|
||||
await execAsync(commandWithLog);
|
||||
}
|
||||
await mechanizeDockerContainer(application);
|
||||
|
||||
const successComment = getIssueComment(
|
||||
application.name,
|
||||
"success",
|
||||
previewDomain,
|
||||
);
|
||||
await updateIssueComment({
|
||||
...issueParams,
|
||||
body: `### Dokploy Preview Deployment\n\n${successComment}`,
|
||||
});
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updatePreviewDeployment(previewDeploymentId, {
|
||||
previewStatus: "done",
|
||||
});
|
||||
} catch (error) {
|
||||
let command = "";
|
||||
|
||||
// Only log details for non-ExecError errors
|
||||
if (!(error instanceof ExecError)) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const encodedMessage = encodeBase64(message);
|
||||
command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`;
|
||||
}
|
||||
|
||||
command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`;
|
||||
const serverId = application.buildServerId || application.serverId;
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
|
||||
const comment = getIssueComment(application.name, "error", previewDomain);
|
||||
await updateIssueComment({
|
||||
...issueParams,
|
||||
body: `### Dokploy Preview Deployment\n\n${comment}`,
|
||||
});
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
await updatePreviewDeployment(previewDeploymentId, {
|
||||
previewStatus: "error",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const getApplicationStats = async (appName: string) => {
|
||||
if (appName === "dokploy") {
|
||||
return await getAdvancedStats(appName);
|
||||
|
||||
@@ -109,7 +109,7 @@ export const getContainersByAppNameMatch = async (
|
||||
try {
|
||||
let result: string[] = [];
|
||||
const cmd =
|
||||
"docker ps -a --format 'CONTAINER ID : {{.ID}} | Name: {{.Names}} | State: {{.State}}'";
|
||||
"docker ps -a --format 'CONTAINER ID : {{.ID}} | Name: {{.Names}} | State: {{.State}} | Status: {{.Status}}'";
|
||||
|
||||
const command =
|
||||
appType === "docker-compose"
|
||||
@@ -148,10 +148,14 @@ export const getContainersByAppNameMatch = async (
|
||||
const state = parts[2]
|
||||
? parts[2].replace("State: ", "").trim()
|
||||
: "No state";
|
||||
|
||||
const status = parts[3] ? parts[3].replace("Status: ", "").trim() : "";
|
||||
|
||||
return {
|
||||
containerId,
|
||||
name,
|
||||
state,
|
||||
status,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -168,7 +172,9 @@ export const getStackContainersByAppName = async (
|
||||
try {
|
||||
let result: string[] = [];
|
||||
|
||||
const command = `docker stack ps ${appName} --format 'CONTAINER ID : {{.ID}} | Name: {{.Name}} | State: {{.DesiredState}} | Node: {{.Node}}'`;
|
||||
const command = `docker stack ps ${appName} --no-trunc --format 'CONTAINER ID : {{.ID}} | Name: {{.Name}} | State: {{.DesiredState}} | Node: {{.Node}} | CurrentState: {{.CurrentState}} | Error: {{.Error}}'`;
|
||||
|
||||
console.log("command ", command);
|
||||
if (serverId) {
|
||||
const { stdout, stderr } = await execAsyncRemote(serverId, command);
|
||||
|
||||
@@ -205,11 +211,17 @@ export const getStackContainersByAppName = async (
|
||||
const node = parts[3]
|
||||
? parts[3].replace("Node: ", "").trim()
|
||||
: "No specific node";
|
||||
const currentState = parts[4]
|
||||
? parts[4].replace("CurrentState: ", "").trim()
|
||||
: "";
|
||||
const error = parts[5] ? parts[5].replace("Error: ", "").trim() : "";
|
||||
return {
|
||||
containerId,
|
||||
name,
|
||||
state,
|
||||
node,
|
||||
currentState,
|
||||
error,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -226,8 +238,7 @@ export const getServiceContainersByAppName = async (
|
||||
try {
|
||||
let result: string[] = [];
|
||||
|
||||
const command = `docker service ps ${appName} --format 'CONTAINER ID : {{.ID}} | Name: {{.Name}} | State: {{.DesiredState}} | Node: {{.Node}}'`;
|
||||
|
||||
const command = `docker service ps ${appName} --no-trunc --format 'CONTAINER ID : {{.ID}} | Name: {{.Name}} | State: {{.DesiredState}} | Node: {{.Node}} | CurrentState: {{.CurrentState}} | Error: {{.Error}}'`;
|
||||
if (serverId) {
|
||||
const { stdout, stderr } = await execAsyncRemote(serverId, command);
|
||||
|
||||
@@ -265,11 +276,18 @@ export const getServiceContainersByAppName = async (
|
||||
const node = parts[3]
|
||||
? parts[3].replace("Node: ", "").trim()
|
||||
: "No specific node";
|
||||
|
||||
const currentState = parts[4]
|
||||
? parts[4].replace("CurrentState: ", "").trim()
|
||||
: "";
|
||||
const error = parts[5] ? parts[5].replace("Error: ", "").trim() : "";
|
||||
return {
|
||||
containerId,
|
||||
name,
|
||||
state,
|
||||
currentState,
|
||||
node,
|
||||
error,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import dns from "node:dns";
|
||||
import { promisify } from "node:util";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { getWebServerSettings } from "@dokploy/server/services/web-server-settings";
|
||||
import { generateRandomDomain } from "@dokploy/server/templates";
|
||||
import { manageDomain } from "@dokploy/server/utils/traefik/domain";
|
||||
import { getWebServerSettings } from "@dokploy/server/services/web-server-settings";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { type apiCreateDomain, domains } from "../db/schema";
|
||||
|
||||
@@ -18,7 +18,7 @@ export type Mariadb = typeof mariadb.$inferSelect;
|
||||
export const createMariadb = async (input: typeof apiCreateMariaDB._type) => {
|
||||
const appName = buildAppName("mariadb", input.appName);
|
||||
|
||||
const valid = await validUniqueServerAppName(input.appName);
|
||||
const valid = await validUniqueServerAppName(appName);
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
type apiCreateGotify,
|
||||
type apiCreateLark,
|
||||
type apiCreateNtfy,
|
||||
type apiCreatePushover,
|
||||
type apiCreateResend,
|
||||
type apiCreateSlack,
|
||||
type apiCreateTelegram,
|
||||
type apiUpdateCustom,
|
||||
@@ -14,6 +16,8 @@ import {
|
||||
type apiUpdateGotify,
|
||||
type apiUpdateLark,
|
||||
type apiUpdateNtfy,
|
||||
type apiUpdatePushover,
|
||||
type apiUpdateResend,
|
||||
type apiUpdateSlack,
|
||||
type apiUpdateTelegram,
|
||||
custom,
|
||||
@@ -23,6 +27,8 @@ import {
|
||||
lark,
|
||||
notifications,
|
||||
ntfy,
|
||||
pushover,
|
||||
resend,
|
||||
slack,
|
||||
telegram,
|
||||
} from "@dokploy/server/db/schema";
|
||||
@@ -409,6 +415,100 @@ export const updateEmailNotification = async (
|
||||
});
|
||||
};
|
||||
|
||||
export const createResendNotification = async (
|
||||
input: typeof apiCreateResend._type,
|
||||
organizationId: string,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const newResend = await tx
|
||||
.insert(resend)
|
||||
.values({
|
||||
apiKey: input.apiKey,
|
||||
fromAddress: input.fromAddress,
|
||||
toAddresses: input.toAddresses,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newResend) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting resend",
|
||||
});
|
||||
}
|
||||
|
||||
const newDestination = await tx
|
||||
.insert(notifications)
|
||||
.values({
|
||||
resendId: newResend.resendId,
|
||||
name: input.name,
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
notificationType: "resend",
|
||||
organizationId: organizationId,
|
||||
serverThreshold: input.serverThreshold,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newDestination) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting notification",
|
||||
});
|
||||
}
|
||||
|
||||
return newDestination;
|
||||
});
|
||||
};
|
||||
|
||||
export const updateResendNotification = async (
|
||||
input: typeof apiUpdateResend._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,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
organizationId: input.organizationId,
|
||||
serverThreshold: input.serverThreshold,
|
||||
})
|
||||
.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(resend)
|
||||
.set({
|
||||
apiKey: input.apiKey,
|
||||
fromAddress: input.fromAddress,
|
||||
toAddresses: input.toAddresses,
|
||||
})
|
||||
.where(eq(resend.resendId, input.resendId))
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
return newDestination;
|
||||
});
|
||||
};
|
||||
|
||||
export const createGotifyNotification = async (
|
||||
input: typeof apiCreateGotify._type,
|
||||
organizationId: string,
|
||||
@@ -690,10 +790,12 @@ export const findNotificationById = async (notificationId: string) => {
|
||||
telegram: true,
|
||||
discord: true,
|
||||
email: true,
|
||||
resend: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
},
|
||||
});
|
||||
if (!notification) {
|
||||
@@ -817,3 +919,99 @@ export const updateNotificationById = async (
|
||||
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const createPushoverNotification = async (
|
||||
input: typeof apiCreatePushover._type,
|
||||
organizationId: string,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const newPushover = await tx
|
||||
.insert(pushover)
|
||||
.values({
|
||||
userKey: input.userKey,
|
||||
apiToken: input.apiToken,
|
||||
priority: input.priority,
|
||||
retry: input.retry,
|
||||
expire: input.expire,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newPushover) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting pushover",
|
||||
});
|
||||
}
|
||||
|
||||
const newDestination = await tx
|
||||
.insert(notifications)
|
||||
.values({
|
||||
pushoverId: newPushover.pushoverId,
|
||||
name: input.name,
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
serverThreshold: input.serverThreshold,
|
||||
notificationType: "pushover",
|
||||
organizationId: organizationId,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newDestination) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting notification",
|
||||
});
|
||||
}
|
||||
|
||||
return newDestination;
|
||||
});
|
||||
};
|
||||
|
||||
export const updatePushoverNotification = async (
|
||||
input: typeof apiUpdatePushover._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,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
organizationId: input.organizationId,
|
||||
serverThreshold: input.serverThreshold,
|
||||
})
|
||||
.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(pushover)
|
||||
.set({
|
||||
userKey: input.userKey,
|
||||
apiToken: input.apiToken,
|
||||
priority: input.priority,
|
||||
retry: input.retry,
|
||||
expire: input.expire,
|
||||
})
|
||||
.where(eq(pushover.pushoverId, input.pushoverId));
|
||||
|
||||
return newDestination;
|
||||
});
|
||||
};
|
||||
|
||||
35
packages/server/src/services/proprietary/sso.ts
Normal file
35
packages/server/src/services/proprietary/sso.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
|
||||
export const getSSOProviders = async () => {
|
||||
const providers = await db.query.ssoProvider.findMany({
|
||||
columns: {
|
||||
id: true,
|
||||
providerId: true,
|
||||
issuer: true,
|
||||
domain: true,
|
||||
oidcConfig: true,
|
||||
samlConfig: true,
|
||||
},
|
||||
});
|
||||
return providers;
|
||||
};
|
||||
|
||||
export const requestToHeaders = (req: {
|
||||
headers?: Record<string, string | string[] | undefined>;
|
||||
}): Headers => {
|
||||
const headers = new Headers();
|
||||
if (req?.headers) {
|
||||
for (const [key, value] of Object.entries(req.headers)) {
|
||||
if (value !== undefined && key.toLowerCase() !== "host") {
|
||||
headers.set(key, Array.isArray(value) ? value.join(", ") : value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return headers;
|
||||
};
|
||||
|
||||
export const normalizeTrustedOrigin = (value: string): string => {
|
||||
// Keep it simple: trim and remove trailing slashes.
|
||||
// e.g. "https://example.com/" -> "https://example.com"
|
||||
return value.trim().replace(/\/+$/, "");
|
||||
};
|
||||
@@ -1,16 +1,19 @@
|
||||
import { readdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { docker } from "@dokploy/server/constants";
|
||||
import {
|
||||
execAsync,
|
||||
execAsyncRemote,
|
||||
} from "@dokploy/server/utils/process/execAsync";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
|
||||
import semver from "semver";
|
||||
import { db } from "../db";
|
||||
import { compose } from "../db/schema";
|
||||
import {
|
||||
initializeStandaloneTraefik,
|
||||
initializeTraefikService,
|
||||
type TraefikOptions,
|
||||
} from "../setup/traefik-setup";
|
||||
|
||||
export interface IUpdateData {
|
||||
latestVersion: string | null;
|
||||
updateAvailable: boolean;
|
||||
@@ -26,19 +29,6 @@ export const getDokployImageTag = () => {
|
||||
return process.env.RELEASE_TAG || "latest";
|
||||
};
|
||||
|
||||
export const getDokployImage = () => {
|
||||
return `dokploy/dokploy:${getDokployImageTag()}`;
|
||||
};
|
||||
|
||||
export const pullLatestRelease = async () => {
|
||||
const stream = await docker.pull(getDokployImage());
|
||||
await new Promise((resolve, reject) => {
|
||||
docker.modem.followProgress(stream, (err, res) =>
|
||||
err ? reject(err) : resolve(res),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
/** Returns Dokploy docker service image digest */
|
||||
export const getServiceImageDigest = async () => {
|
||||
const { stdout } = await execAsync(
|
||||
@@ -55,56 +45,95 @@ export const getServiceImageDigest = async () => {
|
||||
};
|
||||
|
||||
/** Returns latest version number and information whether server update is available by comparing current image's digest against digest for provided image tag via Docker hub API. */
|
||||
export const getUpdateData = async (): Promise<IUpdateData> => {
|
||||
let currentDigest: string;
|
||||
export const getUpdateData = async (
|
||||
currentVersion: string,
|
||||
): Promise<IUpdateData> => {
|
||||
try {
|
||||
currentDigest = await getServiceImageDigest();
|
||||
} catch (error) {
|
||||
// TODO: Docker versions 29.0.0 change the way to get the service image digest, so we need to update this in the future we upgrade to that version.
|
||||
return DEFAULT_UPDATE_DATA;
|
||||
}
|
||||
const baseUrl =
|
||||
"https://hub.docker.com/v2/repositories/dokploy/dokploy/tags";
|
||||
let url: string | null = `${baseUrl}?page_size=100`;
|
||||
let allResults: { digest: string; name: string }[] = [];
|
||||
|
||||
const baseUrl = "https://hub.docker.com/v2/repositories/dokploy/dokploy/tags";
|
||||
let url: string | null = `${baseUrl}?page_size=100`;
|
||||
let allResults: { digest: string; name: string }[] = [];
|
||||
while (url) {
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
// Fetch all tags from Docker Hub
|
||||
while (url) {
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
|
||||
const data = (await response.json()) as {
|
||||
next: string | null;
|
||||
results: { digest: string; name: string }[];
|
||||
};
|
||||
const data = (await response.json()) as {
|
||||
next: string | null;
|
||||
results: { digest: string; name: string }[];
|
||||
};
|
||||
|
||||
allResults = allResults.concat(data.results);
|
||||
url = data?.next;
|
||||
}
|
||||
allResults = allResults.concat(data.results);
|
||||
url = data?.next;
|
||||
}
|
||||
|
||||
const imageTag = getDokployImageTag();
|
||||
const searchedDigest = allResults.find((t) => t.name === imageTag)?.digest;
|
||||
const currentImageTag = getDokployImageTag();
|
||||
|
||||
if (!searchedDigest) {
|
||||
return DEFAULT_UPDATE_DATA;
|
||||
}
|
||||
// Special handling for canary and feature branches
|
||||
// For development versions (canary/feature), don't perform update checks
|
||||
// These are unstable versions that change frequently, and users on these
|
||||
// branches are expected to manually manage updates
|
||||
if (currentImageTag === "canary" || currentImageTag === "feature") {
|
||||
const currentDigest = await getServiceImageDigest();
|
||||
const latestDigest = allResults.find(
|
||||
(t) => t.name === currentImageTag,
|
||||
)?.digest;
|
||||
if (!latestDigest) {
|
||||
return DEFAULT_UPDATE_DATA;
|
||||
}
|
||||
if (currentDigest !== latestDigest) {
|
||||
return {
|
||||
latestVersion: currentImageTag,
|
||||
updateAvailable: true,
|
||||
};
|
||||
}
|
||||
return {
|
||||
latestVersion: currentImageTag,
|
||||
updateAvailable: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (imageTag === "latest") {
|
||||
const versionedTag = allResults.find(
|
||||
(t) => t.digest === searchedDigest && t.name.startsWith("v"),
|
||||
);
|
||||
// For stable versions, use semver comparison
|
||||
// Find the "latest" tag and get its digest
|
||||
const latestTag = allResults.find((t) => t.name === "latest");
|
||||
|
||||
if (!versionedTag) {
|
||||
if (!latestTag) {
|
||||
return DEFAULT_UPDATE_DATA;
|
||||
}
|
||||
|
||||
const { name: latestVersion, digest } = versionedTag;
|
||||
const updateAvailable = digest !== currentDigest;
|
||||
// Find the versioned tag (v0.x.x) that has the same digest as "latest"
|
||||
const latestVersionTag = allResults.find(
|
||||
(t) => t.digest === latestTag.digest && t.name.startsWith("v"),
|
||||
);
|
||||
|
||||
return { latestVersion, updateAvailable };
|
||||
if (!latestVersionTag) {
|
||||
return DEFAULT_UPDATE_DATA;
|
||||
}
|
||||
|
||||
const latestVersion = latestVersionTag.name;
|
||||
|
||||
// Use semver to compare versions for stable releases
|
||||
const cleanedCurrent = semver.clean(currentVersion);
|
||||
const cleanedLatest = semver.clean(latestVersion);
|
||||
|
||||
if (!cleanedCurrent || !cleanedLatest) {
|
||||
return DEFAULT_UPDATE_DATA;
|
||||
}
|
||||
|
||||
// Check if the latest version is greater than the current version
|
||||
const updateAvailable = semver.gt(cleanedLatest, cleanedCurrent);
|
||||
|
||||
return {
|
||||
latestVersion,
|
||||
updateAvailable,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error fetching update data:", error);
|
||||
return DEFAULT_UPDATE_DATA;
|
||||
}
|
||||
const updateAvailable = searchedDigest !== currentDigest;
|
||||
return { latestVersion: imageTag, updateAvailable };
|
||||
};
|
||||
|
||||
interface TreeDataItem {
|
||||
@@ -254,11 +283,22 @@ fi`;
|
||||
export const reloadDockerResource = async (
|
||||
resourceName: string,
|
||||
serverId?: string,
|
||||
version?: string,
|
||||
) => {
|
||||
const resourceType = await getDockerResourceType(resourceName, serverId);
|
||||
let command = "";
|
||||
if (resourceType === "service") {
|
||||
command = `docker service update --force ${resourceName}`;
|
||||
if (resourceName === "dokploy") {
|
||||
const currentImageTag = getDokployImageTag();
|
||||
let imageTag = version;
|
||||
if (currentImageTag === "canary" || currentImageTag === "feature") {
|
||||
imageTag = currentImageTag;
|
||||
}
|
||||
|
||||
command = `docker service update --force --image dokploy/dokploy:${imageTag} ${resourceName}`;
|
||||
} else {
|
||||
command = `docker service update --force ${resourceName}`;
|
||||
}
|
||||
} else if (resourceType === "standalone") {
|
||||
command = `docker restart ${resourceName}`;
|
||||
} else {
|
||||
@@ -402,13 +442,40 @@ export const writeTraefikSetup = async (input: TraefikOptions) => {
|
||||
additionalPorts: input.additionalPorts,
|
||||
serverId: input.serverId,
|
||||
});
|
||||
await reconnectServicesToTraefik(input.serverId);
|
||||
} else if (resourceType === "standalone") {
|
||||
await initializeStandaloneTraefik({
|
||||
env: input.env,
|
||||
additionalPorts: input.additionalPorts,
|
||||
serverId: input.serverId,
|
||||
});
|
||||
|
||||
await reconnectServicesToTraefik(input.serverId);
|
||||
} else {
|
||||
throw new Error("Traefik resource type not found");
|
||||
}
|
||||
};
|
||||
|
||||
export const reconnectServicesToTraefik = async (serverId?: string) => {
|
||||
const composeResult = await db.query.compose.findMany({
|
||||
where: and(
|
||||
...(serverId ? [eq(compose.serverId, serverId)] : []),
|
||||
eq(compose.isolatedDeployment, true),
|
||||
),
|
||||
});
|
||||
|
||||
if (!composeResult) {
|
||||
return;
|
||||
}
|
||||
let commands = "";
|
||||
|
||||
for (const compose of composeResult) {
|
||||
commands += `docker network connect ${compose.appName} $(docker ps --filter "name=dokploy-traefik" -q) >/dev/null 2>&1\n`;
|
||||
}
|
||||
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, commands);
|
||||
} else {
|
||||
await execAsync(commands);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user