mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-10 08:25:22 +02:00
Merge branch 'canary' into feat/add-rhel-flavors
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import Docker from "dockerode";
|
||||
|
||||
export const IS_CLOUD = process.env.IS_CLOUD === "true";
|
||||
|
||||
export const DOKPLOY_DOCKER_API_VERSION =
|
||||
process.env.DOKPLOY_DOCKER_API_VERSION;
|
||||
export const DOKPLOY_DOCKER_HOST = process.env.DOKPLOY_DOCKER_HOST;
|
||||
@@ -10,20 +12,79 @@ export const DOKPLOY_DOCKER_PORT = process.env.DOKPLOY_DOCKER_PORT
|
||||
: undefined;
|
||||
|
||||
export const CLEANUP_CRON_JOB = "50 23 * * *";
|
||||
export const docker = new Docker({
|
||||
...(DOKPLOY_DOCKER_API_VERSION && {
|
||||
version: DOKPLOY_DOCKER_API_VERSION,
|
||||
}),
|
||||
...(DOKPLOY_DOCKER_HOST && {
|
||||
host: DOKPLOY_DOCKER_HOST,
|
||||
}),
|
||||
...(DOKPLOY_DOCKER_PORT && {
|
||||
port: DOKPLOY_DOCKER_PORT,
|
||||
}),
|
||||
});
|
||||
|
||||
type DockerSocketCandidate = {
|
||||
label: string;
|
||||
path: string;
|
||||
};
|
||||
|
||||
const getDockerConfig = (): Docker => {
|
||||
const versionOption = DOKPLOY_DOCKER_API_VERSION
|
||||
? { version: DOKPLOY_DOCKER_API_VERSION }
|
||||
: {};
|
||||
|
||||
// Explicit remote Docker host configuration
|
||||
if (DOKPLOY_DOCKER_HOST) {
|
||||
console.info(
|
||||
`Using remote Docker host: ${DOKPLOY_DOCKER_HOST}${DOKPLOY_DOCKER_PORT ? `:${DOKPLOY_DOCKER_PORT}` : ""}`,
|
||||
);
|
||||
return new Docker({
|
||||
host: DOKPLOY_DOCKER_HOST,
|
||||
...(DOKPLOY_DOCKER_PORT && { port: DOKPLOY_DOCKER_PORT }),
|
||||
...versionOption,
|
||||
});
|
||||
}
|
||||
|
||||
// Local socket auto-detection (Rancher Desktop, Colima, standard Docker)
|
||||
const dockerSocketCandidates: Array<DockerSocketCandidate> = [];
|
||||
|
||||
if (process.env.DOCKER_HOST) {
|
||||
dockerSocketCandidates.push({
|
||||
label: "DOCKER_HOST environment variable",
|
||||
path: process.env.DOCKER_HOST.replace("unix://", ""),
|
||||
});
|
||||
}
|
||||
|
||||
if (process.env.HOME) {
|
||||
dockerSocketCandidates.push({
|
||||
label: "Rancher Desktop socket",
|
||||
path: `${process.env.HOME}/.rd/docker.sock`,
|
||||
});
|
||||
}
|
||||
|
||||
dockerSocketCandidates.push({
|
||||
label: "Standard Docker socket",
|
||||
path: "/var/run/docker.sock",
|
||||
});
|
||||
|
||||
for (const candidate of dockerSocketCandidates) {
|
||||
try {
|
||||
if (candidate.path && fs.existsSync(candidate.path)) {
|
||||
console.info(
|
||||
`Using Docker socket (${candidate.label}): ${candidate.path}`,
|
||||
);
|
||||
return new Docker({
|
||||
socketPath: candidate.path,
|
||||
...versionOption,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.info(
|
||||
`Docker socket initialization failed for ${candidate.label} (${candidate.path}): ${e instanceof Error ? e.message : "Unknown error"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.info(
|
||||
"Using default Docker configuration. You can set the DOCKER_HOST environment variable to specify a custom Docker socket path.",
|
||||
);
|
||||
return new Docker({ ...versionOption });
|
||||
};
|
||||
|
||||
export const docker = getDockerConfig();
|
||||
|
||||
// When not set, use the legacy default so 2FA remains working for users who
|
||||
// enabled it before BETTER_AUTH_SECRET was introduced .
|
||||
// enabled it before BETTER_AUTH_SECRET was introduced.
|
||||
export const BETTER_AUTH_SECRET =
|
||||
process.env.BETTER_AUTH_SECRET || "better-auth-secret-123456789";
|
||||
|
||||
|
||||
@@ -163,6 +163,14 @@ export const member = pgTable("member", {
|
||||
.array()
|
||||
.notNull()
|
||||
.default(sql`ARRAY[]::text[]`),
|
||||
accessedGitProviders: text("accessedGitProviders")
|
||||
.array()
|
||||
.notNull()
|
||||
.default(sql`ARRAY[]::text[]`),
|
||||
accessedServers: text("accessedServers")
|
||||
.array()
|
||||
.notNull()
|
||||
.default(sql`ARRAY[]::text[]`),
|
||||
});
|
||||
|
||||
export const memberRelations = relations(member, ({ one }) => ({
|
||||
|
||||
@@ -115,6 +115,7 @@ export const applications = pgTable("application", {
|
||||
subtitle: text("subtitle"),
|
||||
command: text("command"),
|
||||
args: text("args").array(),
|
||||
icon: text("icon"),
|
||||
refreshToken: text("refreshToken").$defaultFn(() => nanoid()),
|
||||
sourceType: sourceType("sourceType").notNull().default("github"),
|
||||
cleanCache: boolean("cleanCache").default(false),
|
||||
@@ -173,7 +174,7 @@ export const applications = pgTable("application", {
|
||||
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
|
||||
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
|
||||
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
|
||||
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
|
||||
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "number" }),
|
||||
endpointSpecSwarm: json("endpointSpecSwarm").$type<EndpointSpecSwarm>(),
|
||||
ulimitsSwarm: json("ulimitsSwarm").$type<UlimitsSwarm>(),
|
||||
//
|
||||
@@ -368,10 +369,15 @@ const createSchema = createInsertSchema(applications, {
|
||||
watchPaths: z.array(z.string()).optional().optional(),
|
||||
previewLabels: z.array(z.string()).optional(),
|
||||
cleanCache: z.boolean().optional(),
|
||||
stopGracePeriodSwarm: z.bigint().nullable(),
|
||||
stopGracePeriodSwarm: z.number().nullable(),
|
||||
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
|
||||
ulimitsSwarm: UlimitsSwarmSchema.nullable(),
|
||||
enableSubmodules: z.boolean().optional(),
|
||||
icon: z
|
||||
.string()
|
||||
.max(2 * 1024 * 1024, "Icon must be less than 2MB")
|
||||
.nullable()
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const apiCreateApplication = createSchema.pick({
|
||||
|
||||
@@ -15,6 +15,7 @@ import { generateAppName } from ".";
|
||||
import { compose } from "./compose";
|
||||
import { deployments } from "./deployment";
|
||||
import { destinations } from "./destination";
|
||||
import { libsql } from "./libsql";
|
||||
import { mariadb } from "./mariadb";
|
||||
import { mongo } from "./mongo";
|
||||
import { mysql } from "./mysql";
|
||||
@@ -26,6 +27,7 @@ export const databaseType = pgEnum("databaseType", [
|
||||
"mysql",
|
||||
"mongo",
|
||||
"web-server",
|
||||
"libsql",
|
||||
]);
|
||||
|
||||
export const backupType = pgEnum("backupType", ["database", "compose"]);
|
||||
@@ -74,6 +76,9 @@ export const backups = pgTable("backup", {
|
||||
mongoId: text("mongoId").references((): AnyPgColumn => mongo.mongoId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
libsqlId: text("libsqlId").references((): AnyPgColumn => libsql.libsqlId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
userId: text("userId").references(() => user.id),
|
||||
// Only for compose backups
|
||||
metadata: jsonb("metadata").$type<
|
||||
@@ -118,6 +123,10 @@ export const backupsRelations = relations(backups, ({ one, many }) => ({
|
||||
fields: [backups.mongoId],
|
||||
references: [mongo.mongoId],
|
||||
}),
|
||||
libsql: one(libsql, {
|
||||
fields: [backups.libsqlId],
|
||||
references: [libsql.libsqlId],
|
||||
}),
|
||||
user: one(user, {
|
||||
fields: [backups.userId],
|
||||
references: [user.id],
|
||||
@@ -137,11 +146,19 @@ const createSchema = createInsertSchema(backups, {
|
||||
database: z.string().min(1),
|
||||
schedule: z.string(),
|
||||
keepLatestCount: z.number().optional(),
|
||||
databaseType: z.enum(["postgres", "mariadb", "mysql", "mongo", "web-server"]),
|
||||
databaseType: z.enum([
|
||||
"postgres",
|
||||
"mariadb",
|
||||
"mysql",
|
||||
"mongo",
|
||||
"web-server",
|
||||
"libsql",
|
||||
]),
|
||||
postgresId: z.string().optional(),
|
||||
mariadbId: z.string().optional(),
|
||||
mysqlId: z.string().optional(),
|
||||
mongoId: z.string().optional(),
|
||||
libsqlId: z.string().optional(),
|
||||
userId: z.string().optional(),
|
||||
metadata: z.any().optional(),
|
||||
});
|
||||
@@ -157,6 +174,7 @@ export const apiCreateBackup = createSchema.pick({
|
||||
mysqlId: true,
|
||||
postgresId: true,
|
||||
mongoId: true,
|
||||
libsqlId: true,
|
||||
databaseType: true,
|
||||
userId: true,
|
||||
backupType: true,
|
||||
@@ -192,7 +210,14 @@ export const apiUpdateBackup = createSchema
|
||||
|
||||
export const apiRestoreBackup = z.object({
|
||||
databaseId: z.string(),
|
||||
databaseType: z.enum(["postgres", "mysql", "mariadb", "mongo", "web-server"]),
|
||||
databaseType: z.enum([
|
||||
"postgres",
|
||||
"mysql",
|
||||
"mariadb",
|
||||
"mongo",
|
||||
"web-server",
|
||||
"libsql",
|
||||
]),
|
||||
backupType: z.enum(["database", "compose"]),
|
||||
databaseName: z.string().min(1),
|
||||
backupFile: z.string().min(1),
|
||||
|
||||
@@ -56,7 +56,6 @@ export const apiUpdateCertificate = z.object({
|
||||
name: z.string().min(1).optional(),
|
||||
certificateData: z.string().min(1).optional(),
|
||||
privateKey: z.string().min(1).optional(),
|
||||
autoRenew: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const apiDeleteCertificate = z.object({
|
||||
|
||||
@@ -3,6 +3,10 @@ import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
ADDITIONAL_FLAG_ERROR,
|
||||
ADDITIONAL_FLAG_REGEX,
|
||||
} from "../validations/destination";
|
||||
import { organization } from "./account";
|
||||
import { backups } from "./backups";
|
||||
|
||||
@@ -18,6 +22,7 @@ export const destinations = pgTable("destination", {
|
||||
bucket: text("bucket").notNull(),
|
||||
region: text("region").notNull(),
|
||||
endpoint: text("endpoint").notNull(),
|
||||
additionalFlags: text("additionalFlags").array(),
|
||||
organizationId: text("organizationId")
|
||||
.notNull()
|
||||
.references(() => organization.id, { onDelete: "cascade" }),
|
||||
@@ -44,6 +49,9 @@ const createSchema = createInsertSchema(destinations, {
|
||||
endpoint: z.string(),
|
||||
secretAccessKey: z.string(),
|
||||
region: z.string(),
|
||||
additionalFlags: z
|
||||
.array(z.string().regex(ADDITIONAL_FLAG_REGEX, ADDITIONAL_FLAG_ERROR))
|
||||
.default([]),
|
||||
});
|
||||
|
||||
export const apiCreateDestination = createSchema
|
||||
@@ -55,6 +63,7 @@ export const apiCreateDestination = createSchema
|
||||
region: true,
|
||||
endpoint: true,
|
||||
secretAccessKey: true,
|
||||
additionalFlags: true,
|
||||
})
|
||||
.required()
|
||||
.extend({
|
||||
@@ -81,6 +90,7 @@ export const apiUpdateDestination = createSchema
|
||||
secretAccessKey: true,
|
||||
destinationId: true,
|
||||
provider: true,
|
||||
additionalFlags: true,
|
||||
})
|
||||
.required()
|
||||
.extend({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { relations, sql } from "drizzle-orm";
|
||||
import {
|
||||
type AnyPgColumn,
|
||||
boolean,
|
||||
@@ -31,6 +31,7 @@ export const domains = pgTable("domain", {
|
||||
host: text("host").notNull(),
|
||||
https: boolean("https").notNull().default(false),
|
||||
port: integer("port").default(3000),
|
||||
customEntrypoint: text("customEntrypoint"),
|
||||
path: text("path").default("/"),
|
||||
serviceName: text("serviceName"),
|
||||
domainType: domainType("domainType").default("application"),
|
||||
@@ -53,6 +54,7 @@ export const domains = pgTable("domain", {
|
||||
certificateType: certificateType("certificateType").notNull().default("none"),
|
||||
internalPath: text("internalPath").default("/"),
|
||||
stripPath: boolean("stripPath").notNull().default(false),
|
||||
middlewares: text("middlewares").array().default(sql`ARRAY[]::text[]`),
|
||||
});
|
||||
|
||||
export const domainsRelations = relations(domains, ({ one }) => ({
|
||||
@@ -80,6 +82,7 @@ export const apiCreateDomain = createSchema.pick({
|
||||
host: true,
|
||||
path: true,
|
||||
port: true,
|
||||
customEntrypoint: true,
|
||||
https: true,
|
||||
applicationId: true,
|
||||
certificateType: true,
|
||||
@@ -90,6 +93,7 @@ export const apiCreateDomain = createSchema.pick({
|
||||
previewDeploymentId: true,
|
||||
internalPath: true,
|
||||
stripPath: true,
|
||||
middlewares: true,
|
||||
});
|
||||
|
||||
export const apiFindDomain = z.object({
|
||||
@@ -113,6 +117,7 @@ export const apiUpdateDomain = createSchema
|
||||
host: true,
|
||||
path: true,
|
||||
port: true,
|
||||
customEntrypoint: true,
|
||||
https: true,
|
||||
certificateType: true,
|
||||
customCertResolver: true,
|
||||
@@ -120,5 +125,6 @@ export const apiUpdateDomain = createSchema
|
||||
domainType: true,
|
||||
internalPath: true,
|
||||
stripPath: true,
|
||||
middlewares: true,
|
||||
})
|
||||
.merge(createSchema.pick({ domainId: true }).required());
|
||||
|
||||
@@ -4,6 +4,7 @@ import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { applications } from "./application";
|
||||
import { compose } from "./compose";
|
||||
import { libsql } from "./libsql";
|
||||
import { mariadb } from "./mariadb";
|
||||
import { mongo } from "./mongo";
|
||||
import { mysql } from "./mysql";
|
||||
@@ -36,12 +37,13 @@ export const environmentRelations = relations(
|
||||
references: [projects.projectId],
|
||||
}),
|
||||
applications: many(applications),
|
||||
mariadb: many(mariadb),
|
||||
postgres: many(postgres),
|
||||
mysql: many(mysql),
|
||||
redis: many(redis),
|
||||
mongo: many(mongo),
|
||||
compose: many(compose),
|
||||
libsql: many(libsql),
|
||||
mariadb: many(mariadb),
|
||||
mongo: many(mongo),
|
||||
mysql: many(mysql),
|
||||
postgres: many(postgres),
|
||||
redis: many(redis),
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { pgEnum, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { boolean, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { organization } from "./account";
|
||||
@@ -32,6 +32,9 @@ export const gitProvider = pgTable("git_provider", {
|
||||
userId: text("userId")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
sharedWithOrganization: boolean("sharedWithOrganization")
|
||||
.notNull()
|
||||
.default(false),
|
||||
});
|
||||
|
||||
export const gitProviderRelations = relations(gitProvider, ({ one }) => ({
|
||||
@@ -64,3 +67,8 @@ export const gitProviderRelations = relations(gitProvider, ({ one }) => ({
|
||||
export const apiRemoveGitProvider = z.object({
|
||||
gitProviderId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiToggleShareGitProvider = z.object({
|
||||
gitProviderId: z.string().min(1),
|
||||
sharedWithOrganization: z.boolean(),
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export * from "./account";
|
||||
export * from "./ai";
|
||||
export * from "./audit-log";
|
||||
export * from "./application";
|
||||
export * from "./audit-log";
|
||||
export * from "./backups";
|
||||
export * from "./bitbucket";
|
||||
export * from "./certificate";
|
||||
@@ -14,6 +14,7 @@ export * from "./git-provider";
|
||||
export * from "./gitea";
|
||||
export * from "./github";
|
||||
export * from "./gitlab";
|
||||
export * from "./libsql";
|
||||
export * from "./mariadb";
|
||||
export * from "./mongo";
|
||||
export * from "./mount";
|
||||
|
||||
248
packages/server/src/db/schema/libsql.ts
Normal file
248
packages/server/src/db/schema/libsql.ts
Normal file
@@ -0,0 +1,248 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import {
|
||||
bigint,
|
||||
boolean,
|
||||
integer,
|
||||
json,
|
||||
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 { environments } from "./environment";
|
||||
import { mounts } from "./mount";
|
||||
import { server } from "./server";
|
||||
import {
|
||||
applicationStatus,
|
||||
type EndpointSpecSwarm,
|
||||
EndpointSpecSwarmSchema,
|
||||
type HealthCheckSwarm,
|
||||
HealthCheckSwarmSchema,
|
||||
type LabelsSwarm,
|
||||
LabelsSwarmSchema,
|
||||
type NetworkSwarm,
|
||||
NetworkSwarmSchema,
|
||||
type PlacementSwarm,
|
||||
PlacementSwarmSchema,
|
||||
type RestartPolicySwarm,
|
||||
RestartPolicySwarmSchema,
|
||||
type ServiceModeSwarm,
|
||||
ServiceModeSwarmSchema,
|
||||
sqldNode,
|
||||
type UpdateConfigSwarm,
|
||||
UpdateConfigSwarmSchema,
|
||||
} from "./shared";
|
||||
import {
|
||||
DATABASE_PASSWORD_MESSAGE,
|
||||
DATABASE_PASSWORD_REGEX,
|
||||
generateAppName,
|
||||
} from "./utils";
|
||||
|
||||
export const libsql = pgTable("libsql", {
|
||||
libsqlId: text("libsqlId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
name: text("name").notNull(),
|
||||
appName: text("appName")
|
||||
.notNull()
|
||||
.$defaultFn(() => generateAppName("libsql"))
|
||||
.unique(),
|
||||
description: text("description"),
|
||||
databaseUser: text("databaseUser").notNull(),
|
||||
databasePassword: text("databasePassword").notNull(),
|
||||
sqldNode: sqldNode("sqldNode").notNull().default("primary"),
|
||||
sqldPrimaryUrl: text("sqldPrimaryUrl"),
|
||||
enableNamespaces: boolean("enableNamespaces").notNull().default(false),
|
||||
dockerImage: text("dockerImage").notNull(),
|
||||
command: text("command"),
|
||||
env: text("env"),
|
||||
// RESOURCES
|
||||
memoryReservation: text("memoryReservation"),
|
||||
memoryLimit: text("memoryLimit"),
|
||||
cpuReservation: text("cpuReservation"),
|
||||
cpuLimit: text("cpuLimit"),
|
||||
//
|
||||
externalPort: integer("externalPort"),
|
||||
externalGRPCPort: integer("externalGRPCPort"),
|
||||
externalAdminPort: integer("externalAdminPort"),
|
||||
applicationStatus: applicationStatus("applicationStatus")
|
||||
.notNull()
|
||||
.default("idle"),
|
||||
healthCheckSwarm: json("healthCheckSwarm").$type<HealthCheckSwarm>(),
|
||||
restartPolicySwarm: json("restartPolicySwarm").$type<RestartPolicySwarm>(),
|
||||
placementSwarm: json("placementSwarm").$type<PlacementSwarm>(),
|
||||
updateConfigSwarm: json("updateConfigSwarm").$type<UpdateConfigSwarm>(),
|
||||
rollbackConfigSwarm: json("rollbackConfigSwarm").$type<UpdateConfigSwarm>(),
|
||||
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
|
||||
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
|
||||
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
|
||||
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "number" }),
|
||||
endpointSpecSwarm: json("endpointSpecSwarm").$type<EndpointSpecSwarm>(),
|
||||
replicas: integer("replicas").default(1).notNull(),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
|
||||
environmentId: text("environmentId")
|
||||
.notNull()
|
||||
.references(() => environments.environmentId, { onDelete: "cascade" }),
|
||||
serverId: text("serverId").references(() => server.serverId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
});
|
||||
|
||||
export const libsqlRelations = relations(libsql, ({ one, many }) => ({
|
||||
environment: one(environments, {
|
||||
fields: [libsql.environmentId],
|
||||
references: [environments.environmentId],
|
||||
}),
|
||||
backups: many(backups),
|
||||
mounts: many(mounts),
|
||||
server: one(server, {
|
||||
fields: [libsql.serverId],
|
||||
references: [server.serverId],
|
||||
}),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(libsql, {
|
||||
libsqlId: z.string(),
|
||||
name: z.string().min(1),
|
||||
appName: z.string().min(1),
|
||||
createdAt: z.string(),
|
||||
databaseUser: z.string().min(1),
|
||||
databasePassword: z.string().regex(DATABASE_PASSWORD_REGEX, {
|
||||
message: DATABASE_PASSWORD_MESSAGE,
|
||||
}),
|
||||
sqldNode: z.enum(sqldNode.enumValues),
|
||||
sqldPrimaryUrl: z.string().nullable(),
|
||||
enableNamespaces: z.boolean().default(false),
|
||||
dockerImage: z
|
||||
.string()
|
||||
.default("ghcr.io/tursodatabase/libsql-server:v0.24.32"),
|
||||
command: z.string().optional(),
|
||||
env: z.string().optional(),
|
||||
memoryReservation: z.string().optional(),
|
||||
memoryLimit: z.string().optional(),
|
||||
cpuReservation: z.string().optional(),
|
||||
cpuLimit: z.string().optional(),
|
||||
environmentId: z.string(),
|
||||
applicationStatus: z.enum(["idle", "running", "done", "error"]),
|
||||
externalPort: z.number(),
|
||||
externalGRPCPort: z.number(),
|
||||
externalAdminPort: z.number(),
|
||||
description: z.string().optional(),
|
||||
serverId: z.string().optional(),
|
||||
healthCheckSwarm: HealthCheckSwarmSchema.nullable(),
|
||||
restartPolicySwarm: RestartPolicySwarmSchema.nullable(),
|
||||
placementSwarm: PlacementSwarmSchema.nullable(),
|
||||
updateConfigSwarm: UpdateConfigSwarmSchema.nullable(),
|
||||
rollbackConfigSwarm: UpdateConfigSwarmSchema.nullable(),
|
||||
modeSwarm: ServiceModeSwarmSchema.nullable(),
|
||||
labelsSwarm: LabelsSwarmSchema.nullable(),
|
||||
networkSwarm: NetworkSwarmSchema.nullable(),
|
||||
stopGracePeriodSwarm: z.number().nullable(),
|
||||
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
|
||||
});
|
||||
|
||||
export const apiCreateLibsql = createSchema
|
||||
.pick({
|
||||
name: true,
|
||||
appName: true,
|
||||
dockerImage: true,
|
||||
environmentId: true,
|
||||
description: true,
|
||||
databaseUser: true,
|
||||
databasePassword: true,
|
||||
sqldNode: true,
|
||||
sqldPrimaryUrl: true,
|
||||
enableNamespaces: true,
|
||||
serverId: true,
|
||||
})
|
||||
.required()
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.sqldNode === "replica" && !data.sqldPrimaryUrl) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["sqldPrimaryUrl"],
|
||||
message: "sqldPrimaryUrl is required when sqldNode is 'replica'.",
|
||||
});
|
||||
}
|
||||
if (data.sqldNode !== "replica" && data.sqldPrimaryUrl) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["sqldPrimaryUrl"],
|
||||
message:
|
||||
"sqldPrimaryUrl should not be provided when sqldNode is not 'replica'.",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const apiFindOneLibsql = z.object({
|
||||
libsqlId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiChangeLibsqlStatus = createSchema
|
||||
.pick({
|
||||
libsqlId: true,
|
||||
applicationStatus: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiSaveEnvironmentVariablesLibsql = createSchema
|
||||
.pick({
|
||||
libsqlId: true,
|
||||
env: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiSaveExternalPortsLibsql = createSchema
|
||||
.pick({
|
||||
libsqlId: true,
|
||||
externalPort: true,
|
||||
externalGRPCPort: true,
|
||||
externalAdminPort: true,
|
||||
})
|
||||
.required({ libsqlId: true })
|
||||
.superRefine((data, ctx) => {
|
||||
if (
|
||||
data.externalPort === null &&
|
||||
data.externalGRPCPort === null &&
|
||||
data.externalAdminPort === null
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message:
|
||||
"Either externalPort, externalGRPCPort or externalAdminPort must be provided.",
|
||||
path: ["externalPort", "externalGRPCPort", "externalAdminPort"],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const apiDeployLibsql = createSchema
|
||||
.pick({
|
||||
libsqlId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiResetLibsql = createSchema
|
||||
.pick({
|
||||
libsqlId: true,
|
||||
appName: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiUpdateLibsql = createSchema
|
||||
.partial()
|
||||
.extend({
|
||||
libsqlId: z.string().min(1),
|
||||
})
|
||||
.omit({ serverId: true });
|
||||
|
||||
export const apiRebuildLibsql = createSchema
|
||||
.pick({
|
||||
libsqlId: true,
|
||||
})
|
||||
.required();
|
||||
@@ -28,7 +28,13 @@ import {
|
||||
type UpdateConfigSwarm,
|
||||
UpdateConfigSwarmSchema,
|
||||
} from "./shared";
|
||||
import { APP_NAME_MESSAGE, APP_NAME_REGEX, generateAppName } from "./utils";
|
||||
import {
|
||||
APP_NAME_MESSAGE,
|
||||
APP_NAME_REGEX,
|
||||
DATABASE_PASSWORD_MESSAGE,
|
||||
DATABASE_PASSWORD_REGEX,
|
||||
generateAppName,
|
||||
} from "./utils";
|
||||
|
||||
export const mariadb = pgTable("mariadb", {
|
||||
mariadbId: text("mariadbId")
|
||||
@@ -67,7 +73,7 @@ export const mariadb = pgTable("mariadb", {
|
||||
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
|
||||
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
|
||||
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
|
||||
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
|
||||
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "number" }),
|
||||
endpointSpecSwarm: json("endpointSpecSwarm").$type<EndpointSpecSwarm>(),
|
||||
ulimitsSwarm: json("ulimitsSwarm").$type<UlimitsSwarm>(),
|
||||
replicas: integer("replicas").default(1).notNull(),
|
||||
@@ -108,17 +114,13 @@ const createSchema = createInsertSchema(mariadb, {
|
||||
createdAt: z.string(),
|
||||
databaseName: z.string().min(1),
|
||||
databaseUser: z.string().min(1),
|
||||
databasePassword: z
|
||||
.string()
|
||||
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
|
||||
message:
|
||||
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
|
||||
}),
|
||||
databasePassword: z.string().regex(DATABASE_PASSWORD_REGEX, {
|
||||
message: DATABASE_PASSWORD_MESSAGE,
|
||||
}),
|
||||
databaseRootPassword: z
|
||||
.string()
|
||||
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
|
||||
message:
|
||||
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
|
||||
.regex(DATABASE_PASSWORD_REGEX, {
|
||||
message: DATABASE_PASSWORD_MESSAGE,
|
||||
})
|
||||
.optional(),
|
||||
dockerImage: z.string().default("mariadb:6"),
|
||||
@@ -142,7 +144,7 @@ const createSchema = createInsertSchema(mariadb, {
|
||||
modeSwarm: ServiceModeSwarmSchema.nullable(),
|
||||
labelsSwarm: LabelsSwarmSchema.nullable(),
|
||||
networkSwarm: NetworkSwarmSchema.nullable(),
|
||||
stopGracePeriodSwarm: z.bigint().nullable(),
|
||||
stopGracePeriodSwarm: z.number().nullable(),
|
||||
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
|
||||
ulimitsSwarm: UlimitsSwarmSchema.nullable(),
|
||||
});
|
||||
|
||||
@@ -35,7 +35,13 @@ import {
|
||||
type UpdateConfigSwarm,
|
||||
UpdateConfigSwarmSchema,
|
||||
} from "./shared";
|
||||
import { APP_NAME_MESSAGE, APP_NAME_REGEX, generateAppName } from "./utils";
|
||||
import {
|
||||
APP_NAME_MESSAGE,
|
||||
APP_NAME_REGEX,
|
||||
DATABASE_PASSWORD_MESSAGE,
|
||||
DATABASE_PASSWORD_REGEX,
|
||||
generateAppName,
|
||||
} from "./utils";
|
||||
|
||||
export const mongo = pgTable("mongo", {
|
||||
mongoId: text("mongoId")
|
||||
@@ -50,7 +56,7 @@ export const mongo = pgTable("mongo", {
|
||||
description: text("description"),
|
||||
databaseUser: text("databaseUser").notNull(),
|
||||
databasePassword: text("databasePassword").notNull(),
|
||||
dockerImage: text("dockerImage").notNull(),
|
||||
dockerImage: text("dockerImage").notNull().default("mongo:8"),
|
||||
command: text("command"),
|
||||
args: text("args").array(),
|
||||
env: text("env"),
|
||||
@@ -70,7 +76,7 @@ export const mongo = pgTable("mongo", {
|
||||
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
|
||||
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
|
||||
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
|
||||
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
|
||||
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "number" }),
|
||||
endpointSpecSwarm: json("endpointSpecSwarm").$type<EndpointSpecSwarm>(),
|
||||
ulimitsSwarm: json("ulimitsSwarm").$type<UlimitsSwarm>(),
|
||||
replicas: integer("replicas").default(1).notNull(),
|
||||
@@ -110,12 +116,9 @@ const createSchema = createInsertSchema(mongo, {
|
||||
createdAt: z.string(),
|
||||
mongoId: z.string(),
|
||||
name: z.string().min(1),
|
||||
databasePassword: z
|
||||
.string()
|
||||
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
|
||||
message:
|
||||
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
|
||||
}),
|
||||
databasePassword: z.string().regex(DATABASE_PASSWORD_REGEX, {
|
||||
message: DATABASE_PASSWORD_MESSAGE,
|
||||
}),
|
||||
databaseUser: z.string().min(1),
|
||||
dockerImage: z.string().default("mongo:15"),
|
||||
command: z.string().optional(),
|
||||
@@ -139,7 +142,7 @@ const createSchema = createInsertSchema(mongo, {
|
||||
modeSwarm: ServiceModeSwarmSchema.nullable(),
|
||||
labelsSwarm: LabelsSwarmSchema.nullable(),
|
||||
networkSwarm: NetworkSwarmSchema.nullable(),
|
||||
stopGracePeriodSwarm: z.bigint().nullable(),
|
||||
stopGracePeriodSwarm: z.number().nullable(),
|
||||
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
|
||||
ulimitsSwarm: UlimitsSwarmSchema.nullable(),
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { applications } from "./application";
|
||||
import { compose } from "./compose";
|
||||
import { libsql } from "./libsql";
|
||||
import { mariadb } from "./mariadb";
|
||||
import { mongo } from "./mongo";
|
||||
import { mysql } from "./mysql";
|
||||
@@ -19,8 +20,11 @@ export const serviceType = pgEnum("serviceType", [
|
||||
"mongo",
|
||||
"redis",
|
||||
"compose",
|
||||
"libsql",
|
||||
]);
|
||||
|
||||
export type ServiceType = (typeof serviceType.enumValues)[number];
|
||||
|
||||
export const mountType = pgEnum("mountType", ["bind", "volume", "file"]);
|
||||
|
||||
export const mounts = pgTable("mount", {
|
||||
@@ -39,7 +43,10 @@ export const mounts = pgTable("mount", {
|
||||
() => applications.applicationId,
|
||||
{ onDelete: "cascade" },
|
||||
),
|
||||
postgresId: text("postgresId").references(() => postgres.postgresId, {
|
||||
composeId: text("composeId").references(() => compose.composeId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
libsqlId: text("libsqlId").references(() => libsql.libsqlId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
mariadbId: text("mariadbId").references(() => mariadb.mariadbId, {
|
||||
@@ -51,10 +58,10 @@ export const mounts = pgTable("mount", {
|
||||
mysqlId: text("mysqlId").references(() => mysql.mysqlId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
redisId: text("redisId").references(() => redis.redisId, {
|
||||
postgresId: text("postgresId").references(() => postgres.postgresId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
composeId: text("composeId").references(() => compose.composeId, {
|
||||
redisId: text("redisId").references(() => redis.redisId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
});
|
||||
@@ -64,9 +71,13 @@ export const MountssRelations = relations(mounts, ({ one }) => ({
|
||||
fields: [mounts.applicationId],
|
||||
references: [applications.applicationId],
|
||||
}),
|
||||
postgres: one(postgres, {
|
||||
fields: [mounts.postgresId],
|
||||
references: [postgres.postgresId],
|
||||
compose: one(compose, {
|
||||
fields: [mounts.composeId],
|
||||
references: [compose.composeId],
|
||||
}),
|
||||
libsql: one(libsql, {
|
||||
fields: [mounts.libsqlId],
|
||||
references: [libsql.libsqlId],
|
||||
}),
|
||||
mariadb: one(mariadb, {
|
||||
fields: [mounts.mariadbId],
|
||||
@@ -80,14 +91,14 @@ export const MountssRelations = relations(mounts, ({ one }) => ({
|
||||
fields: [mounts.mysqlId],
|
||||
references: [mysql.mysqlId],
|
||||
}),
|
||||
postgres: one(postgres, {
|
||||
fields: [mounts.postgresId],
|
||||
references: [postgres.postgresId],
|
||||
}),
|
||||
redis: one(redis, {
|
||||
fields: [mounts.redisId],
|
||||
references: [redis.redisId],
|
||||
}),
|
||||
compose: one(compose, {
|
||||
fields: [mounts.composeId],
|
||||
references: [compose.composeId],
|
||||
}),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(mounts, {
|
||||
@@ -107,13 +118,10 @@ const createSchema = createInsertSchema(mounts, {
|
||||
"mongo",
|
||||
"redis",
|
||||
"compose",
|
||||
"libsql",
|
||||
]),
|
||||
});
|
||||
|
||||
export type ServiceType = NonNullable<
|
||||
z.infer<typeof createSchema>["serviceType"]
|
||||
>;
|
||||
|
||||
export const apiCreateMount = createSchema
|
||||
.pick({
|
||||
type: true,
|
||||
@@ -121,8 +129,8 @@ export const apiCreateMount = createSchema
|
||||
volumeName: true,
|
||||
content: true,
|
||||
mountPath: true,
|
||||
serviceType: true,
|
||||
filePath: true,
|
||||
serviceType: true,
|
||||
})
|
||||
.extend({
|
||||
serviceId: z.string().min(1),
|
||||
@@ -141,23 +149,13 @@ export const apiRemoveMount = createSchema
|
||||
// })
|
||||
.required();
|
||||
|
||||
export const apiFindMountByApplicationId = createSchema
|
||||
.extend({
|
||||
serviceId: z.string().min(1),
|
||||
serviceType: z.enum([
|
||||
"application",
|
||||
"postgres",
|
||||
"mysql",
|
||||
"mariadb",
|
||||
"mongo",
|
||||
"redis",
|
||||
"compose",
|
||||
]),
|
||||
})
|
||||
.pick({
|
||||
serviceId: true,
|
||||
serviceType: true,
|
||||
});
|
||||
export const apiFindMountByApplicationId = z.object({
|
||||
serviceType: z
|
||||
.string()
|
||||
.min(1)
|
||||
.transform((val) => val as ServiceType),
|
||||
serviceId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiUpdateMount = createSchema.partial().extend({
|
||||
mountId: z.string().min(1),
|
||||
|
||||
@@ -28,7 +28,13 @@ import {
|
||||
type UpdateConfigSwarm,
|
||||
UpdateConfigSwarmSchema,
|
||||
} from "./shared";
|
||||
import { APP_NAME_MESSAGE, APP_NAME_REGEX, generateAppName } from "./utils";
|
||||
import {
|
||||
APP_NAME_MESSAGE,
|
||||
APP_NAME_REGEX,
|
||||
DATABASE_PASSWORD_MESSAGE,
|
||||
DATABASE_PASSWORD_REGEX,
|
||||
generateAppName,
|
||||
} from "./utils";
|
||||
|
||||
export const mysql = pgTable("mysql", {
|
||||
mysqlId: text("mysqlId")
|
||||
@@ -65,7 +71,7 @@ export const mysql = pgTable("mysql", {
|
||||
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
|
||||
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
|
||||
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
|
||||
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
|
||||
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "number" }),
|
||||
endpointSpecSwarm: json("endpointSpecSwarm").$type<EndpointSpecSwarm>(),
|
||||
ulimitsSwarm: json("ulimitsSwarm").$type<UlimitsSwarm>(),
|
||||
replicas: integer("replicas").default(1).notNull(),
|
||||
@@ -106,17 +112,13 @@ const createSchema = createInsertSchema(mysql, {
|
||||
name: z.string().min(1),
|
||||
databaseName: z.string().min(1),
|
||||
databaseUser: z.string().min(1),
|
||||
databasePassword: z
|
||||
.string()
|
||||
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
|
||||
message:
|
||||
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
|
||||
}),
|
||||
databasePassword: z.string().regex(DATABASE_PASSWORD_REGEX, {
|
||||
message: DATABASE_PASSWORD_MESSAGE,
|
||||
}),
|
||||
databaseRootPassword: z
|
||||
.string()
|
||||
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
|
||||
message:
|
||||
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
|
||||
.regex(DATABASE_PASSWORD_REGEX, {
|
||||
message: DATABASE_PASSWORD_MESSAGE,
|
||||
})
|
||||
.optional(),
|
||||
dockerImage: z.string().default("mysql:8"),
|
||||
@@ -139,7 +141,7 @@ const createSchema = createInsertSchema(mysql, {
|
||||
modeSwarm: ServiceModeSwarmSchema.nullable(),
|
||||
labelsSwarm: LabelsSwarmSchema.nullable(),
|
||||
networkSwarm: NetworkSwarmSchema.nullable(),
|
||||
stopGracePeriodSwarm: z.bigint().nullable(),
|
||||
stopGracePeriodSwarm: z.number().nullable(),
|
||||
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
|
||||
ulimitsSwarm: UlimitsSwarmSchema.nullable(),
|
||||
});
|
||||
|
||||
@@ -20,6 +20,7 @@ export const notificationType = pgEnum("notificationType", [
|
||||
"resend",
|
||||
"gotify",
|
||||
"ntfy",
|
||||
"mattermost",
|
||||
"pushover",
|
||||
"custom",
|
||||
"lark",
|
||||
@@ -37,6 +38,7 @@ export const notifications = pgTable("notification", {
|
||||
databaseBackup: boolean("databaseBackup").notNull().default(false),
|
||||
volumeBackup: boolean("volumeBackup").notNull().default(false),
|
||||
dokployRestart: boolean("dokployRestart").notNull().default(false),
|
||||
dokployBackup: boolean("dokployBackup").notNull().default(false),
|
||||
dockerCleanup: boolean("dockerCleanup").notNull().default(false),
|
||||
serverThreshold: boolean("serverThreshold").notNull().default(false),
|
||||
notificationType: notificationType("notificationType").notNull(),
|
||||
@@ -64,6 +66,9 @@ export const notifications = pgTable("notification", {
|
||||
ntfyId: text("ntfyId").references(() => ntfy.ntfyId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
mattermostId: text("mattermostId").references(() => mattermost.mattermostId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
customId: text("customId").references(() => custom.customId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
@@ -154,6 +159,16 @@ export const ntfy = pgTable("ntfy", {
|
||||
priority: integer("priority").notNull().default(3),
|
||||
});
|
||||
|
||||
export const mattermost = pgTable("mattermost", {
|
||||
mattermostId: text("mattermostId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
webhookUrl: text("webhookUrl").notNull(),
|
||||
channel: text("channel"),
|
||||
username: text("username"),
|
||||
});
|
||||
|
||||
export const custom = pgTable("custom", {
|
||||
customId: text("customId")
|
||||
.notNull()
|
||||
@@ -220,6 +235,10 @@ export const notificationsRelations = relations(notifications, ({ one }) => ({
|
||||
fields: [notifications.ntfyId],
|
||||
references: [ntfy.ntfyId],
|
||||
}),
|
||||
mattermost: one(mattermost, {
|
||||
fields: [notifications.mattermostId],
|
||||
references: [mattermost.mattermostId],
|
||||
}),
|
||||
custom: one(custom, {
|
||||
fields: [notifications.customId],
|
||||
references: [custom.customId],
|
||||
@@ -248,6 +267,7 @@ export const apiCreateSlack = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
dokployBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
@@ -276,6 +296,7 @@ export const apiCreateTelegram = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
dokployBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
@@ -306,6 +327,7 @@ export const apiCreateDiscord = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
dokployBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
@@ -337,6 +359,7 @@ export const apiCreateEmail = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
dokployBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
@@ -373,6 +396,7 @@ export const apiCreateResend = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
dokployBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
@@ -403,6 +427,7 @@ export const apiCreateGotify = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
dokployBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
@@ -437,6 +462,7 @@ export const apiCreateNtfy = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
dokployBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
@@ -464,6 +490,53 @@ export const apiTestNtfyConnection = apiCreateNtfy.pick({
|
||||
priority: true,
|
||||
});
|
||||
|
||||
export const apiCreateMattermost = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
dokployBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
appDeploy: true,
|
||||
dockerCleanup: true,
|
||||
serverThreshold: true,
|
||||
})
|
||||
.extend({
|
||||
webhookUrl: z.string().url(),
|
||||
channel: z.string().optional(),
|
||||
username: z.string().optional(),
|
||||
})
|
||||
.required({
|
||||
name: true,
|
||||
webhookUrl: true,
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
dokployBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
appDeploy: true,
|
||||
dockerCleanup: true,
|
||||
serverThreshold: true,
|
||||
});
|
||||
|
||||
export const apiUpdateMattermost = apiCreateMattermost.partial().extend({
|
||||
notificationId: z.string().min(1),
|
||||
mattermostId: z.string().min(1),
|
||||
organizationId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiTestMattermostConnection = apiCreateMattermost
|
||||
.pick({
|
||||
webhookUrl: true,
|
||||
channel: true,
|
||||
username: true,
|
||||
})
|
||||
.extend({
|
||||
channel: z.string().optional(),
|
||||
username: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiFindOneNotification = z.object({
|
||||
notificationId: z.string().min(1),
|
||||
});
|
||||
@@ -472,6 +545,7 @@ export const apiCreateCustom = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
dokployBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
@@ -499,6 +573,7 @@ export const apiCreateLark = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
dokployBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
@@ -525,6 +600,7 @@ export const apiCreateTeams = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
dokployBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
@@ -551,6 +627,7 @@ export const apiCreatePushover = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
dokployBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
@@ -585,6 +662,7 @@ export const apiUpdatePushover = z.object({
|
||||
expire: z.number().min(1).max(10800).nullish(),
|
||||
appBuildError: z.boolean().optional(),
|
||||
databaseBackup: z.boolean().optional(),
|
||||
dokployBackup: z.boolean().optional(),
|
||||
volumeBackup: z.boolean().optional(),
|
||||
dokployRestart: z.boolean().optional(),
|
||||
name: z.string().optional(),
|
||||
|
||||
@@ -28,7 +28,13 @@ import {
|
||||
type UpdateConfigSwarm,
|
||||
UpdateConfigSwarmSchema,
|
||||
} from "./shared";
|
||||
import { APP_NAME_MESSAGE, APP_NAME_REGEX, generateAppName } from "./utils";
|
||||
import {
|
||||
APP_NAME_MESSAGE,
|
||||
APP_NAME_REGEX,
|
||||
DATABASE_PASSWORD_MESSAGE,
|
||||
DATABASE_PASSWORD_REGEX,
|
||||
generateAppName,
|
||||
} from "./utils";
|
||||
|
||||
export const postgres = pgTable("postgres", {
|
||||
postgresId: text("postgresId")
|
||||
@@ -65,7 +71,7 @@ export const postgres = pgTable("postgres", {
|
||||
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
|
||||
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
|
||||
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
|
||||
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
|
||||
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "number" }),
|
||||
endpointSpecSwarm: json("endpointSpecSwarm").$type<EndpointSpecSwarm>(),
|
||||
ulimitsSwarm: json("ulimitsSwarm").$type<UlimitsSwarm>(),
|
||||
replicas: integer("replicas").default(1).notNull(),
|
||||
@@ -103,12 +109,9 @@ const createSchema = createInsertSchema(postgres, {
|
||||
.max(63)
|
||||
.regex(APP_NAME_REGEX, APP_NAME_MESSAGE)
|
||||
.optional(),
|
||||
databasePassword: z
|
||||
.string()
|
||||
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
|
||||
message:
|
||||
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
|
||||
}),
|
||||
databasePassword: z.string().regex(DATABASE_PASSWORD_REGEX, {
|
||||
message: DATABASE_PASSWORD_MESSAGE,
|
||||
}),
|
||||
databaseName: z.string().min(1),
|
||||
databaseUser: z.string().min(1),
|
||||
dockerImage: z.string().default("postgres:18"),
|
||||
@@ -133,7 +136,7 @@ const createSchema = createInsertSchema(postgres, {
|
||||
modeSwarm: ServiceModeSwarmSchema.nullable(),
|
||||
labelsSwarm: LabelsSwarmSchema.nullable(),
|
||||
networkSwarm: NetworkSwarmSchema.nullable(),
|
||||
stopGracePeriodSwarm: z.bigint().nullable(),
|
||||
stopGracePeriodSwarm: z.number().nullable(),
|
||||
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
|
||||
ulimitsSwarm: UlimitsSwarmSchema.nullable(),
|
||||
});
|
||||
|
||||
@@ -64,7 +64,7 @@ export const redis = pgTable("redis", {
|
||||
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
|
||||
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
|
||||
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
|
||||
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
|
||||
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "number" }),
|
||||
endpointSpecSwarm: json("endpointSpecSwarm").$type<EndpointSpecSwarm>(),
|
||||
ulimitsSwarm: json("ulimitsSwarm").$type<UlimitsSwarm>(),
|
||||
replicas: integer("replicas").default(1).notNull(),
|
||||
@@ -121,7 +121,7 @@ const createSchema = createInsertSchema(redis, {
|
||||
modeSwarm: ServiceModeSwarmSchema.nullable(),
|
||||
labelsSwarm: LabelsSwarmSchema.nullable(),
|
||||
networkSwarm: NetworkSwarmSchema.nullable(),
|
||||
stopGracePeriodSwarm: z.bigint().nullable(),
|
||||
stopGracePeriodSwarm: z.number().nullable(),
|
||||
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
|
||||
ulimitsSwarm: UlimitsSwarmSchema.nullable(),
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ import { applications } from "./application";
|
||||
import { certificates } from "./certificate";
|
||||
import { compose } from "./compose";
|
||||
import { deployments } from "./deployment";
|
||||
import { libsql } from "./libsql";
|
||||
import { mariadb } from "./mariadb";
|
||||
import { mongo } from "./mongo";
|
||||
import { mysql } from "./mysql";
|
||||
@@ -116,6 +117,7 @@ export const serverRelations = relations(server, ({ one, many }) => ({
|
||||
relationName: "applicationBuildServer",
|
||||
}),
|
||||
compose: many(compose),
|
||||
libsql: many(libsql),
|
||||
redis: many(redis),
|
||||
mariadb: many(mariadb),
|
||||
mongo: many(mongo),
|
||||
|
||||
@@ -16,6 +16,8 @@ export const certificateType = pgEnum("certificateType", [
|
||||
|
||||
export const triggerType = pgEnum("triggerType", ["push", "tag"]);
|
||||
|
||||
export const sqldNode = pgEnum("sqldNode", ["primary", "replica"]);
|
||||
|
||||
export interface HealthCheckSwarm {
|
||||
Test?: string[] | undefined;
|
||||
Interval?: number | undefined;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import { relations } from "drizzle-orm";
|
||||
import { relations, sql } from "drizzle-orm";
|
||||
import {
|
||||
boolean,
|
||||
integer,
|
||||
@@ -65,7 +65,11 @@ export const user = pgTable("user", {
|
||||
stripeCustomerId: text("stripeCustomerId"),
|
||||
stripeSubscriptionId: text("stripeSubscriptionId"),
|
||||
serversQuantity: integer("serversQuantity").notNull().default(0),
|
||||
isEnterpriseCloud: boolean("isEnterpriseCloud").notNull().default(false),
|
||||
trustedOrigins: text("trustedOrigins").array(),
|
||||
bookmarkedTemplates: text("bookmarkedTemplates")
|
||||
.array()
|
||||
.default(sql`ARRAY[]::text[]`),
|
||||
});
|
||||
|
||||
export const usersRelations = relations(user, ({ one, many }) => ({
|
||||
@@ -87,7 +91,9 @@ const createSchema = createInsertSchema(user, {
|
||||
}).omit({
|
||||
role: true,
|
||||
trustedOrigins: true,
|
||||
bookmarkedTemplates: true,
|
||||
isValidEnterpriseLicense: true,
|
||||
isEnterpriseCloud: true,
|
||||
});
|
||||
|
||||
export const apiCreateUserInvitation = createSchema.pick({}).extend({
|
||||
@@ -126,6 +132,8 @@ export const apiAssignPermissions = createSchema
|
||||
accessedProjects: z.array(z.string()).optional(),
|
||||
accessedEnvironments: z.array(z.string()).optional(),
|
||||
accessedServices: z.array(z.string()).optional(),
|
||||
accessedGitProviders: z.array(z.string()).optional(),
|
||||
accessedServers: z.array(z.string()).optional(),
|
||||
canCreateProjects: z.boolean().optional(),
|
||||
canCreateServices: z.boolean().optional(),
|
||||
canDeleteProjects: z.boolean().optional(),
|
||||
|
||||
@@ -12,6 +12,13 @@ export const APP_NAME_REGEX = /^[a-zA-Z0-9._-]+$/;
|
||||
export const APP_NAME_MESSAGE =
|
||||
"App name can only contain letters, numbers, dots, underscores and hyphens";
|
||||
|
||||
/** Database password: blocks shell-dangerous characters like $ ! ' " \ / and spaces. */
|
||||
export const DATABASE_PASSWORD_REGEX =
|
||||
/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/;
|
||||
|
||||
export const DATABASE_PASSWORD_MESSAGE =
|
||||
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility";
|
||||
|
||||
export const generateAppName = (type: string) => {
|
||||
const verb = faker.hacker.verb().replace(/ /g, "-");
|
||||
const adjective = faker.hacker.adjective().replace(/ /g, "-");
|
||||
|
||||
@@ -7,6 +7,7 @@ import { applications } from "./application";
|
||||
import { compose } from "./compose";
|
||||
import { deployments } from "./deployment";
|
||||
import { destinations } from "./destination";
|
||||
import { libsql } from "./libsql";
|
||||
import { mariadb } from "./mariadb";
|
||||
import { mongo } from "./mongo";
|
||||
import { serviceType } from "./mount";
|
||||
@@ -53,6 +54,9 @@ export const volumeBackups = pgTable("volume_backup", {
|
||||
redisId: text("redisId").references(() => redis.redisId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
libsqlId: text("libsqlId").references(() => libsql.libsqlId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
composeId: text("composeId").references(() => compose.composeId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
@@ -93,6 +97,10 @@ export const volumeBackupsRelations = relations(
|
||||
fields: [volumeBackups.redisId],
|
||||
references: [redis.redisId],
|
||||
}),
|
||||
libsql: one(libsql, {
|
||||
fields: [volumeBackups.libsqlId],
|
||||
references: [libsql.libsqlId],
|
||||
}),
|
||||
compose: one(compose, {
|
||||
fields: [volumeBackups.composeId],
|
||||
references: [compose.composeId],
|
||||
|
||||
3
packages/server/src/db/validations/destination.ts
Normal file
3
packages/server/src/db/validations/destination.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const ADDITIONAL_FLAG_REGEX = /^--[a-zA-Z0-9-]+(=[a-zA-Z0-9._:/@-]+)?$/;
|
||||
export const ADDITIONAL_FLAG_ERROR =
|
||||
"Invalid flag format. Must start with -- (e.g. --s3-sign-accept-encoding=false)";
|
||||
@@ -20,6 +20,7 @@ export const domain = z
|
||||
https: z.boolean().optional(),
|
||||
certificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
|
||||
customCertResolver: z.string(),
|
||||
middlewares: z.array(z.string()).optional(),
|
||||
})
|
||||
.superRefine((input, ctx) => {
|
||||
if (input.https && !input.certificateType) {
|
||||
@@ -83,6 +84,7 @@ export const domainCompose = z
|
||||
certificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
|
||||
customCertResolver: z.string(),
|
||||
serviceName: z.string().min(1, { message: "Service name is required" }),
|
||||
middlewares: z.array(z.string()).optional(),
|
||||
})
|
||||
.superRefine((input, ctx) => {
|
||||
if (input.https && !input.certificateType) {
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
export type TemplateProps = {
|
||||
projectName: string;
|
||||
applicationName: string;
|
||||
databaseType: "postgres" | "mysql" | "mongodb" | "mariadb";
|
||||
databaseType: "postgres" | "mysql" | "mongodb" | "mariadb" | "libsql";
|
||||
type: "error" | "success";
|
||||
errorMessage?: string;
|
||||
date: string;
|
||||
|
||||
106
packages/server/src/emails/emails/dokploy-backup.tsx
Normal file
106
packages/server/src/emails/emails/dokploy-backup.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import {
|
||||
Body,
|
||||
Container,
|
||||
Head,
|
||||
Heading,
|
||||
Html,
|
||||
Img,
|
||||
Preview,
|
||||
Section,
|
||||
Tailwind,
|
||||
Text,
|
||||
} from "@react-email/components";
|
||||
|
||||
export type TemplateProps = {
|
||||
type: "error" | "success";
|
||||
errorMessage?: string;
|
||||
date: string;
|
||||
backupSize?: string;
|
||||
};
|
||||
|
||||
export const DokployBackupEmail = ({
|
||||
type = "success",
|
||||
errorMessage,
|
||||
date = "2023-05-01T00:00:00.000Z",
|
||||
backupSize,
|
||||
}: TemplateProps) => {
|
||||
const previewText = `Dokploy instance backup was ${type === "success" ? "successful ✅" : "failed ❌"}`;
|
||||
|
||||
return (
|
||||
<Html>
|
||||
<Preview>{previewText}</Preview>
|
||||
<Tailwind
|
||||
config={{
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
brand: "#007291",
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Head />
|
||||
<Body className="bg-white my-auto mx-auto font-sans px-2">
|
||||
<Container className="border border-solid border-[#eaeaea] rounded-lg my-[40px] mx-auto p-[20px] max-w-[465px]">
|
||||
<Section className="mt-[32px]">
|
||||
<Img
|
||||
src={
|
||||
"https://raw.githubusercontent.com/Dokploy/dokploy/refs/heads/canary/apps/dokploy/logo.png"
|
||||
}
|
||||
width="100"
|
||||
height="50"
|
||||
alt="Dokploy"
|
||||
className="my-0 mx-auto"
|
||||
/>
|
||||
</Section>
|
||||
<Heading className="text-black text-[24px] font-normal text-center p-0 my-[30px] mx-0">
|
||||
Dokploy Instance Backup
|
||||
</Heading>
|
||||
<Text className="text-black text-[14px] leading-[24px]">
|
||||
Hello,
|
||||
</Text>
|
||||
<Text className="text-black text-[14px] leading-[24px]">
|
||||
Your Dokploy instance backup was{" "}
|
||||
{type === "success"
|
||||
? "successful ✅"
|
||||
: "failed. Please check the error message below. ❌"}
|
||||
.
|
||||
</Text>
|
||||
<Section className="flex text-black text-[14px] leading-[24px] bg-[#F4F4F5] rounded-lg p-2">
|
||||
<Text className="!leading-3 font-bold">Details: </Text>
|
||||
<Text className="!leading-3">
|
||||
Backup Type: <strong>Complete Dokploy Instance</strong>
|
||||
</Text>
|
||||
<Text className="!leading-3">
|
||||
Content: <strong>/etc/dokploy + PostgreSQL Database</strong>
|
||||
</Text>
|
||||
{backupSize && (
|
||||
<Text className="!leading-3">
|
||||
Backup Size: <strong>{backupSize}</strong>
|
||||
</Text>
|
||||
)}
|
||||
<Text className="!leading-3">
|
||||
Date: <strong>{date}</strong>
|
||||
</Text>
|
||||
<Text className="!leading-3">
|
||||
Status:{" "}
|
||||
<strong>{type === "success" ? "Successful" : "Failed"}</strong>
|
||||
</Text>
|
||||
</Section>
|
||||
{type === "error" && errorMessage ? (
|
||||
<Section className="flex text-black text-[14px] mt-4 leading-[24px] bg-[#F4F4F5] rounded-lg p-2">
|
||||
<Text className="!leading-3 font-bold">Reason: </Text>
|
||||
<Text className="text-[12px] leading-[24px]">
|
||||
{errorMessage || "Error message not provided"}
|
||||
</Text>
|
||||
</Section>
|
||||
) : null}
|
||||
</Container>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>
|
||||
);
|
||||
};
|
||||
|
||||
export default DokployBackupEmail;
|
||||
@@ -22,7 +22,8 @@ export type TemplateProps = {
|
||||
| "mongodb"
|
||||
| "mariadb"
|
||||
| "redis"
|
||||
| "compose";
|
||||
| "compose"
|
||||
| "libsql";
|
||||
type: "error" | "success";
|
||||
errorMessage?: string;
|
||||
backupSize?: string;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export * from "./auth/random-password";
|
||||
export * from "./constants/index";
|
||||
export * from "./db/constants";
|
||||
export * from "./db/validations/destination";
|
||||
export * from "./db/validations/domain";
|
||||
export * from "./db/validations/index";
|
||||
export * from "./lib/auth";
|
||||
@@ -22,6 +23,7 @@ export * from "./services/git-provider";
|
||||
export * from "./services/gitea";
|
||||
export * from "./services/github";
|
||||
export * from "./services/gitlab";
|
||||
export * from "./services/libsql";
|
||||
export * from "./services/mariadb";
|
||||
export * from "./services/mongo";
|
||||
export * from "./services/mount";
|
||||
@@ -67,6 +69,7 @@ export * from "./utils/access-log/types";
|
||||
export * from "./utils/access-log/utils";
|
||||
export * from "./utils/backups/compose";
|
||||
export * from "./utils/backups/index";
|
||||
export * from "./utils/backups/libsql";
|
||||
export * from "./utils/backups/mariadb";
|
||||
export * from "./utils/backups/mongo";
|
||||
export * from "./utils/backups/mysql";
|
||||
@@ -118,7 +121,7 @@ export * from "./utils/providers/raw";
|
||||
export * from "./utils/schedules/index";
|
||||
export * from "./utils/schedules/utils";
|
||||
export * from "./utils/servers/remote-docker";
|
||||
export * from "./utils/startup/cancell-deployments";
|
||||
export * from "./utils/startup/cancel-deployments";
|
||||
export * from "./utils/tracking/hubspot";
|
||||
export * from "./utils/traefik/application";
|
||||
export * from "./utils/traefik/domain";
|
||||
|
||||
@@ -37,7 +37,7 @@ export const statements = {
|
||||
environmentEnvVars: ["read", "write"],
|
||||
server: ["read", "create", "delete"],
|
||||
registry: ["read", "create", "delete"],
|
||||
certificate: ["read", "create", "delete"],
|
||||
certificate: ["read", "create", "update", "delete"],
|
||||
backup: ["read", "create", "update", "delete", "restore"],
|
||||
volumeBackup: ["read", "create", "update", "delete", "restore"],
|
||||
schedule: ["read", "create", "update", "delete"],
|
||||
@@ -102,7 +102,7 @@ export const ownerRole = ac.newRole({
|
||||
environmentEnvVars: ["read", "write"],
|
||||
server: ["read", "create", "delete"],
|
||||
registry: ["read", "create", "delete"],
|
||||
certificate: ["read", "create", "delete"],
|
||||
certificate: ["read", "create", "update", "delete"],
|
||||
backup: ["read", "create", "update", "delete", "restore"],
|
||||
volumeBackup: ["read", "create", "update", "delete", "restore"],
|
||||
schedule: ["read", "create", "update", "delete"],
|
||||
@@ -139,7 +139,7 @@ export const adminRole = ac.newRole({
|
||||
environmentEnvVars: ["read", "write"],
|
||||
server: ["read", "create", "delete"],
|
||||
registry: ["read", "create", "delete"],
|
||||
certificate: ["read", "create", "delete"],
|
||||
certificate: ["read", "create", "update", "delete"],
|
||||
backup: ["read", "create", "update", "delete", "restore"],
|
||||
volumeBackup: ["read", "create", "update", "delete", "restore"],
|
||||
schedule: ["read", "create", "update", "delete"],
|
||||
|
||||
@@ -33,6 +33,7 @@ export const findBackupById = async (backupId: string) => {
|
||||
mysql: true,
|
||||
mariadb: true,
|
||||
mongo: true,
|
||||
libsql: true,
|
||||
destination: true,
|
||||
compose: true,
|
||||
},
|
||||
@@ -72,7 +73,7 @@ export const removeBackupById = async (backupId: string) => {
|
||||
|
||||
export const findBackupsByDbId = async (
|
||||
id: string,
|
||||
type: "postgres" | "mysql" | "mariadb" | "mongo",
|
||||
type: "postgres" | "mysql" | "mariadb" | "mongo" | "libsql",
|
||||
) => {
|
||||
const result = await db.query.backups.findMany({
|
||||
where: eq(backups[`${type}Id`], id),
|
||||
@@ -81,6 +82,7 @@ export const findBackupsByDbId = async (
|
||||
mysql: true,
|
||||
mariadb: true,
|
||||
mongo: true,
|
||||
libsql: true,
|
||||
destination: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -126,3 +126,36 @@ const createCertificateFiles = async (certificate: Certificate) => {
|
||||
fs.writeFileSync(configFile, yamlConfig);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateCertificate = async (
|
||||
certificateId: string,
|
||||
updates: {
|
||||
name?: string;
|
||||
certificateData?: string;
|
||||
privateKey?: string;
|
||||
},
|
||||
) => {
|
||||
const updated = await db
|
||||
.update(certificates)
|
||||
.set({
|
||||
...updates,
|
||||
})
|
||||
.where(eq(certificates.certificateId, certificateId))
|
||||
.returning();
|
||||
|
||||
if (!updated || updated[0] === undefined) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Failed to update the certificate",
|
||||
});
|
||||
}
|
||||
|
||||
const cert = updated[0];
|
||||
|
||||
// If cert data or private key changed, rewrite files
|
||||
if (updates.certificateData || updates.privateKey) {
|
||||
await createCertificateFiles(cert);
|
||||
}
|
||||
|
||||
return cert;
|
||||
};
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
} from "@dokploy/server/utils/process/execAsync";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { format } from "date-fns";
|
||||
import { desc, eq, and, inArray, or, sql } from "drizzle-orm";
|
||||
import { and, desc, eq, inArray, or, sql } from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
import {
|
||||
type Application,
|
||||
@@ -177,7 +177,7 @@ export const createDeployment = async (
|
||||
status: "error",
|
||||
logPath: "",
|
||||
description: deployment.description || "",
|
||||
errorMessage: `An error have occured: ${error instanceof Error ? error.message : error}`,
|
||||
errorMessage: `An error have occurred: ${error instanceof Error ? error.message : error}`,
|
||||
startedAt: new Date().toISOString(),
|
||||
finishedAt: new Date().toISOString(),
|
||||
})
|
||||
@@ -257,7 +257,7 @@ export const createDeploymentPreview = async (
|
||||
status: "error",
|
||||
logPath: "",
|
||||
description: deployment.description || "",
|
||||
errorMessage: `An error have occured: ${error instanceof Error ? error.message : error}`,
|
||||
errorMessage: `An error have occurred: ${error instanceof Error ? error.message : error}`,
|
||||
startedAt: new Date().toISOString(),
|
||||
finishedAt: new Date().toISOString(),
|
||||
})
|
||||
@@ -334,7 +334,7 @@ echo "Initializing deployment\n" >> ${logFilePath};
|
||||
status: "error",
|
||||
logPath: "",
|
||||
description: deployment.description || "",
|
||||
errorMessage: `An error have occured: ${error instanceof Error ? error.message : error}`,
|
||||
errorMessage: `An error have occurred: ${error instanceof Error ? error.message : error}`,
|
||||
startedAt: new Date().toISOString(),
|
||||
finishedAt: new Date().toISOString(),
|
||||
})
|
||||
@@ -418,7 +418,7 @@ echo "Initializing backup\n" >> ${logFilePath};
|
||||
status: "error",
|
||||
logPath: "",
|
||||
description: deployment.description || "",
|
||||
errorMessage: `An error have occured: ${error instanceof Error ? error.message : error}`,
|
||||
errorMessage: `An error have occurred: ${error instanceof Error ? error.message : error}`,
|
||||
startedAt: new Date().toISOString(),
|
||||
finishedAt: new Date().toISOString(),
|
||||
})
|
||||
@@ -493,7 +493,7 @@ export const createDeploymentSchedule = async (
|
||||
status: "error",
|
||||
logPath: "",
|
||||
description: deployment.description || "",
|
||||
errorMessage: `An error have occured: ${error instanceof Error ? error.message : error}`,
|
||||
errorMessage: `An error have occurred: ${error instanceof Error ? error.message : error}`,
|
||||
startedAt: new Date().toISOString(),
|
||||
finishedAt: new Date().toISOString(),
|
||||
})
|
||||
@@ -578,7 +578,7 @@ export const createDeploymentVolumeBackup = async (
|
||||
status: "error",
|
||||
logPath: "",
|
||||
description: deployment.description || "",
|
||||
errorMessage: `An error have occured: ${error instanceof Error ? error.message : error}`,
|
||||
errorMessage: `An error have occurred: ${error instanceof Error ? error.message : error}`,
|
||||
startedAt: new Date().toISOString(),
|
||||
finishedAt: new Date().toISOString(),
|
||||
})
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { z } from "zod";
|
||||
|
||||
export type Destination = typeof destinations.$inferSelect;
|
||||
|
||||
export const createDestintation = async (
|
||||
export const createDestination = async (
|
||||
input: z.infer<typeof apiCreateDestination>,
|
||||
organizationId: string,
|
||||
) => {
|
||||
|
||||
@@ -371,6 +371,21 @@ export const containerRestart = async (containerId: string) => {
|
||||
} catch {}
|
||||
};
|
||||
|
||||
export const containerRemove = async (
|
||||
containerId: string,
|
||||
serverId?: string,
|
||||
) => {
|
||||
const command = `docker rm -f ${containerId}`;
|
||||
const { stderr } = serverId
|
||||
? await execAsyncRemote(serverId, command)
|
||||
: await execAsync(command);
|
||||
|
||||
if (stderr) {
|
||||
console.error(`Error: ${stderr}`);
|
||||
throw new Error(stderr);
|
||||
}
|
||||
};
|
||||
|
||||
export const getSwarmNodes = async (serverId?: string) => {
|
||||
try {
|
||||
let stdout = "";
|
||||
@@ -397,7 +412,9 @@ export const getSwarmNodes = async (serverId?: string) => {
|
||||
.split("\n")
|
||||
.map((line) => JSON.parse(line));
|
||||
return nodesArray;
|
||||
} catch {}
|
||||
} catch (error) {
|
||||
console.error("getSwarmNodes error:", error);
|
||||
}
|
||||
};
|
||||
|
||||
export const getNodeInfo = async (nodeId: string, serverId?: string) => {
|
||||
@@ -448,6 +465,10 @@ export const getNodeApplications = async (serverId?: string) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!stdout.trim()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const appArray = stdout
|
||||
.trim()
|
||||
.split("\n")
|
||||
@@ -455,13 +476,19 @@ export const getNodeApplications = async (serverId?: string) => {
|
||||
.filter((service) => !service.Name.startsWith("dokploy-"));
|
||||
|
||||
return appArray;
|
||||
} catch {}
|
||||
} catch (error) {
|
||||
console.error("getNodeApplications error:", error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const getApplicationInfo = async (
|
||||
appNames: string[],
|
||||
serverId?: string,
|
||||
) => {
|
||||
if (appNames.length === 0) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
@@ -482,11 +509,84 @@ export const getApplicationInfo = async (
|
||||
return;
|
||||
}
|
||||
|
||||
if (!stdout.trim()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const appArray = stdout
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((line) => JSON.parse(line));
|
||||
|
||||
return appArray;
|
||||
} catch {}
|
||||
} catch (error) {
|
||||
console.error("getApplicationInfo error:", error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const getAllContainerStats = async (serverId?: string) => {
|
||||
try {
|
||||
let stdout = "";
|
||||
const command =
|
||||
'docker stats --no-stream --format \'{"BlockIO":"{{.BlockIO}}","CPUPerc":"{{.CPUPerc}}","Container":"{{.Container}}","ID":"{{.ID}}","MemPerc":"{{.MemPerc}}","MemUsage":"{{.MemUsage}}","Name":"{{.Name}}","NetIO":"{{.NetIO}}"}\'';
|
||||
|
||||
if (serverId) {
|
||||
const result = await execAsyncRemote(serverId, command);
|
||||
stdout = result.stdout;
|
||||
} else {
|
||||
const result = await execAsync(command);
|
||||
stdout = result.stdout;
|
||||
}
|
||||
|
||||
if (!stdout.trim()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const stats = stdout
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((line) => JSON.parse(line));
|
||||
|
||||
return stats;
|
||||
} catch (error) {
|
||||
console.error("getAllContainerStats error:", error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const uploadFileToContainer = async (
|
||||
containerId: string,
|
||||
fileBuffer: Buffer,
|
||||
fileName: string,
|
||||
destinationPath: string,
|
||||
serverId?: string | null,
|
||||
): Promise<void> => {
|
||||
const containerIdRegex = /^[a-zA-Z0-9.\-_]+$/;
|
||||
if (!containerIdRegex.test(containerId)) {
|
||||
throw new Error("Invalid container ID");
|
||||
}
|
||||
|
||||
// Ensure destination path starts with /
|
||||
const normalizedPath = destinationPath.startsWith("/")
|
||||
? destinationPath
|
||||
: `/${destinationPath}`;
|
||||
|
||||
const base64Content = fileBuffer.toString("base64");
|
||||
const tempFileName = `dokploy-upload-${Date.now()}-${fileName.replace(/[^a-zA-Z0-9.-]/g, "_")}`;
|
||||
const tempPath = `/tmp/${tempFileName}`;
|
||||
|
||||
const command = `echo '${base64Content}' | base64 -d > "${tempPath}" && docker cp "${tempPath}" "${containerId}:${normalizedPath}" ; rm -f "${tempPath}"`;
|
||||
|
||||
try {
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to upload file to container: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -59,6 +59,7 @@ export const findEnvironmentById = async (environmentId: string) => {
|
||||
applicationStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
icon: true,
|
||||
},
|
||||
},
|
||||
mariadb: {
|
||||
@@ -169,6 +170,24 @@ export const findEnvironmentById = async (environmentId: string) => {
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
libsql: {
|
||||
with: {
|
||||
server: {
|
||||
columns: {
|
||||
name: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
libsqlId: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
applicationStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
project: true,
|
||||
},
|
||||
});
|
||||
@@ -193,6 +212,7 @@ export const findEnvironmentsByProjectId = async (projectId: string) => {
|
||||
postgres: true,
|
||||
redis: true,
|
||||
compose: true,
|
||||
libsql: true,
|
||||
project: true,
|
||||
},
|
||||
columns: {
|
||||
@@ -211,6 +231,7 @@ const environmentHasServices = (
|
||||
return (
|
||||
(env.applications?.length ?? 0) > 0 ||
|
||||
(env.compose?.length ?? 0) > 0 ||
|
||||
(env.libsql?.length ?? 0) > 0 ||
|
||||
(env.mariadb?.length ?? 0) > 0 ||
|
||||
(env.mongo?.length ?? 0) > 0 ||
|
||||
(env.mysql?.length ?? 0) > 0 ||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { gitProvider } from "@dokploy/server/db/schema";
|
||||
import { gitProvider, member } from "@dokploy/server/db/schema";
|
||||
import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
|
||||
export type GitProvider = typeof gitProvider.$inferSelect;
|
||||
|
||||
@@ -41,3 +42,48 @@ export const updateGitProvider = async (
|
||||
.returning()
|
||||
.then((response) => response[0]);
|
||||
};
|
||||
|
||||
export const getAccessibleGitProviderIds = async (session: {
|
||||
userId: string;
|
||||
activeOrganizationId: string;
|
||||
}): Promise<Set<string>> => {
|
||||
const { userId, activeOrganizationId } = session;
|
||||
|
||||
const allOrgProviders = await db.query.gitProvider.findMany({
|
||||
where: eq(gitProvider.organizationId, activeOrganizationId),
|
||||
columns: {
|
||||
gitProviderId: true,
|
||||
userId: true,
|
||||
sharedWithOrganization: true,
|
||||
},
|
||||
});
|
||||
|
||||
const memberRecord = await db.query.member.findFirst({
|
||||
where: and(
|
||||
eq(member.userId, userId),
|
||||
eq(member.organizationId, activeOrganizationId),
|
||||
),
|
||||
columns: { accessedGitProviders: true, role: true },
|
||||
});
|
||||
|
||||
if (memberRecord?.role === "owner" || memberRecord?.role === "admin") {
|
||||
return new Set(allOrgProviders.map((p) => p.gitProviderId));
|
||||
}
|
||||
|
||||
const licensed = await hasValidLicense(activeOrganizationId);
|
||||
const assignedSet = licensed
|
||||
? new Set(memberRecord?.accessedGitProviders ?? [])
|
||||
: new Set<string>();
|
||||
|
||||
const result = new Set<string>();
|
||||
for (const p of allOrgProviders) {
|
||||
if (
|
||||
p.userId === userId ||
|
||||
p.sharedWithOrganization ||
|
||||
assignedSet.has(p.gitProviderId)
|
||||
) {
|
||||
result.add(p.gitProviderId);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
162
packages/server/src/services/libsql.ts
Normal file
162
packages/server/src/services/libsql.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
type apiCreateLibsql,
|
||||
backups,
|
||||
buildAppName,
|
||||
libsql,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { generatePassword } from "@dokploy/server/templates";
|
||||
import { buildLibsql } from "@dokploy/server/utils/databases/libsql";
|
||||
import { pullImage } from "@dokploy/server/utils/docker/utils";
|
||||
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq, getTableColumns } from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
import { validUniqueServerAppName } from "./project";
|
||||
|
||||
export type Libsql = typeof libsql.$inferSelect;
|
||||
|
||||
export const createLibsql = async (input: z.infer<typeof apiCreateLibsql>) => {
|
||||
const appName = buildAppName("libsql", input.appName);
|
||||
|
||||
const valid = await validUniqueServerAppName(input.appName);
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Service with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
|
||||
const newLibsql = await db
|
||||
.insert(libsql)
|
||||
.values({
|
||||
...input,
|
||||
databasePassword: input.databasePassword
|
||||
? input.databasePassword
|
||||
: generatePassword(),
|
||||
appName,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newLibsql) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting libsql database",
|
||||
});
|
||||
}
|
||||
|
||||
return newLibsql;
|
||||
};
|
||||
|
||||
// https://github.com/drizzle-team/drizzle-orm/discussions/1483#discussioncomment-7523881
|
||||
export const findLibsqlById = async (libsqlId: string) => {
|
||||
const result = await db.query.libsql.findFirst({
|
||||
where: eq(libsql.libsqlId, libsqlId),
|
||||
with: {
|
||||
environment: {
|
||||
with: {
|
||||
project: true,
|
||||
},
|
||||
},
|
||||
mounts: true,
|
||||
server: true,
|
||||
backups: {
|
||||
with: {
|
||||
destination: true,
|
||||
deployments: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!result) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Libsql not found",
|
||||
});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const updateLibsqlById = async (
|
||||
libsqlId: string,
|
||||
libsqlData: Partial<Libsql>,
|
||||
) => {
|
||||
const { appName, ...rest } = libsqlData;
|
||||
const result = await db
|
||||
.update(libsql)
|
||||
.set({
|
||||
...rest,
|
||||
})
|
||||
.where(eq(libsql.libsqlId, libsqlId))
|
||||
.returning();
|
||||
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const removeLibsqlById = async (libsqlId: string) => {
|
||||
const result = await db
|
||||
.delete(libsql)
|
||||
.where(eq(libsql.libsqlId, libsqlId))
|
||||
.returning();
|
||||
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const findLibsqlByBackupId = async (backupId: string) => {
|
||||
const result = await db
|
||||
.select({
|
||||
...getTableColumns(libsql),
|
||||
})
|
||||
.from(libsql)
|
||||
.innerJoin(backups, eq(libsql.libsqlId, backups.libsqlId))
|
||||
.where(eq(backups.backupId, backupId))
|
||||
.limit(1);
|
||||
|
||||
if (!result || !result[0]) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Libsql not found",
|
||||
});
|
||||
}
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const deployLibsql = async (
|
||||
libsqlId: string,
|
||||
onData?: (data: any) => void,
|
||||
) => {
|
||||
const libsql = await findLibsqlById(libsqlId);
|
||||
try {
|
||||
await updateLibsqlById(libsqlId, {
|
||||
applicationStatus: "running",
|
||||
});
|
||||
onData?.("Starting libsql deployment...");
|
||||
if (libsql.serverId) {
|
||||
await execAsyncRemote(
|
||||
libsql.serverId,
|
||||
`docker pull ${libsql.dockerImage}`,
|
||||
onData,
|
||||
);
|
||||
} else {
|
||||
await pullImage(libsql.dockerImage, onData);
|
||||
}
|
||||
|
||||
await buildLibsql(libsql);
|
||||
await updateLibsqlById(libsqlId, {
|
||||
applicationStatus: "done",
|
||||
});
|
||||
onData?.("Deployment completed successfully!");
|
||||
} catch (error) {
|
||||
onData?.(`Error: ${error}`);
|
||||
await updateLibsqlById(libsqlId, {
|
||||
applicationStatus: "error",
|
||||
});
|
||||
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: `Error on deploy libsql${error}`,
|
||||
});
|
||||
}
|
||||
return libsql;
|
||||
};
|
||||
@@ -32,8 +32,11 @@ export const createMount = async (input: z.infer<typeof apiCreateMount>) => {
|
||||
...(input.serviceType === "application" && {
|
||||
applicationId: serviceId,
|
||||
}),
|
||||
...(input.serviceType === "postgres" && {
|
||||
postgresId: serviceId,
|
||||
...(input.serviceType === "compose" && {
|
||||
composeId: serviceId,
|
||||
}),
|
||||
...(input.serviceType === "libsql" && {
|
||||
libsqlId: serviceId,
|
||||
}),
|
||||
...(input.serviceType === "mariadb" && {
|
||||
mariadbId: serviceId,
|
||||
@@ -44,12 +47,12 @@ export const createMount = async (input: z.infer<typeof apiCreateMount>) => {
|
||||
...(input.serviceType === "mysql" && {
|
||||
mysqlId: serviceId,
|
||||
}),
|
||||
...(input.serviceType === "postgres" && {
|
||||
postgresId: serviceId,
|
||||
}),
|
||||
...(input.serviceType === "redis" && {
|
||||
redisId: serviceId,
|
||||
}),
|
||||
...(input.serviceType === "compose" && {
|
||||
composeId: serviceId,
|
||||
}),
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
@@ -115,7 +118,16 @@ export const findMountById = async (mountId: string) => {
|
||||
},
|
||||
},
|
||||
},
|
||||
postgres: {
|
||||
compose: {
|
||||
with: {
|
||||
environment: {
|
||||
with: {
|
||||
project: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
libsql: {
|
||||
with: {
|
||||
environment: {
|
||||
with: {
|
||||
@@ -151,7 +163,7 @@ export const findMountById = async (mountId: string) => {
|
||||
},
|
||||
},
|
||||
},
|
||||
redis: {
|
||||
postgres: {
|
||||
with: {
|
||||
environment: {
|
||||
with: {
|
||||
@@ -160,7 +172,7 @@ export const findMountById = async (mountId: string) => {
|
||||
},
|
||||
},
|
||||
},
|
||||
compose: {
|
||||
redis: {
|
||||
with: {
|
||||
environment: {
|
||||
with: {
|
||||
@@ -186,8 +198,11 @@ export const findMountOrganizationId = async (mountId: string) => {
|
||||
if (mount.application) {
|
||||
return mount.application.environment.project.organizationId;
|
||||
}
|
||||
if (mount.postgres) {
|
||||
return mount.postgres.environment.project.organizationId;
|
||||
if (mount.compose) {
|
||||
return mount.compose.environment.project.organizationId;
|
||||
}
|
||||
if (mount.libsql) {
|
||||
return mount.libsql.environment.project.organizationId;
|
||||
}
|
||||
if (mount.mariadb) {
|
||||
return mount.mariadb.environment.project.organizationId;
|
||||
@@ -198,13 +213,13 @@ export const findMountOrganizationId = async (mountId: string) => {
|
||||
if (mount.mysql) {
|
||||
return mount.mysql.environment.project.organizationId;
|
||||
}
|
||||
if (mount.postgres) {
|
||||
return mount.postgres.environment.project.organizationId;
|
||||
}
|
||||
if (mount.redis) {
|
||||
return mount.redis.environment.project.organizationId;
|
||||
}
|
||||
|
||||
if (mount.compose) {
|
||||
return mount.compose.environment.project.organizationId;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -248,8 +263,8 @@ export const findMountsByApplicationId = async (
|
||||
case "application":
|
||||
sqlChunks.push(eq(mounts.applicationId, serviceId));
|
||||
break;
|
||||
case "postgres":
|
||||
sqlChunks.push(eq(mounts.postgresId, serviceId));
|
||||
case "libsql":
|
||||
sqlChunks.push(eq(mounts.libsqlId, serviceId));
|
||||
break;
|
||||
case "mariadb":
|
||||
sqlChunks.push(eq(mounts.mariadbId, serviceId));
|
||||
@@ -260,6 +275,9 @@ export const findMountsByApplicationId = async (
|
||||
case "mysql":
|
||||
sqlChunks.push(eq(mounts.mysqlId, serviceId));
|
||||
break;
|
||||
case "postgres":
|
||||
sqlChunks.push(eq(mounts.postgresId, serviceId));
|
||||
break;
|
||||
case "redis":
|
||||
sqlChunks.push(eq(mounts.redisId, serviceId));
|
||||
break;
|
||||
@@ -362,6 +380,10 @@ export const getBaseFilesPath = async (mountId: string) => {
|
||||
const { COMPOSE_PATH } = paths(!!mount.compose.serverId);
|
||||
appName = mount.compose.appName;
|
||||
absoluteBasePath = path.resolve(COMPOSE_PATH);
|
||||
} else if (mount.serviceType === "libsql" && mount.libsql) {
|
||||
const { APPLICATIONS_PATH } = paths(!!mount.libsql.serverId);
|
||||
absoluteBasePath = path.resolve(APPLICATIONS_PATH);
|
||||
appName = mount.libsql.appName;
|
||||
}
|
||||
directoryPath = path.join(absoluteBasePath, appName, "files");
|
||||
|
||||
@@ -391,6 +413,9 @@ export const getServerId = async (mount: MountNested) => {
|
||||
if (mount.serviceType === "compose" && mount?.compose?.serverId) {
|
||||
return mount.compose.serverId;
|
||||
}
|
||||
if (mount.serviceType === "libsql" && mount?.libsql?.serverId) {
|
||||
return mount.libsql.serverId;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
type apiCreateEmail,
|
||||
type apiCreateGotify,
|
||||
type apiCreateLark,
|
||||
type apiCreateMattermost,
|
||||
type apiCreateNtfy,
|
||||
type apiCreatePushover,
|
||||
type apiCreateResend,
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
type apiUpdateEmail,
|
||||
type apiUpdateGotify,
|
||||
type apiUpdateLark,
|
||||
type apiUpdateMattermost,
|
||||
type apiUpdateNtfy,
|
||||
type apiUpdatePushover,
|
||||
type apiUpdateResend,
|
||||
@@ -27,6 +29,7 @@ import {
|
||||
email,
|
||||
gotify,
|
||||
lark,
|
||||
mattermost,
|
||||
notifications,
|
||||
ntfy,
|
||||
pushover,
|
||||
@@ -70,6 +73,7 @@ export const createSlackNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployBackup: input.dokployBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
@@ -102,6 +106,7 @@ export const updateSlackNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployBackup: input.dokployBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
@@ -163,6 +168,7 @@ export const createTelegramNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployBackup: input.dokployBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
@@ -195,6 +201,7 @@ export const updateTelegramNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployBackup: input.dokployBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
@@ -256,6 +263,7 @@ export const createDiscordNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployBackup: input.dokployBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
@@ -288,6 +296,7 @@ export const updateDiscordNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployBackup: input.dokployBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
@@ -352,6 +361,7 @@ export const createEmailNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployBackup: input.dokployBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
@@ -384,6 +394,7 @@ export const updateEmailNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployBackup: input.dokployBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
@@ -449,6 +460,7 @@ export const createResendNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployBackup: input.dokployBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
@@ -481,6 +493,7 @@ export const updateResendNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployBackup: input.dokployBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
@@ -544,6 +557,7 @@ export const createGotifyNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployBackup: input.dokployBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
@@ -575,6 +589,7 @@ export const updateGotifyNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployBackup: input.dokployBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
@@ -636,6 +651,7 @@ export const createNtfyNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployBackup: input.dokployBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
@@ -667,6 +683,7 @@ export const updateNtfyNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployBackup: input.dokployBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
@@ -726,6 +743,8 @@ export const createCustomNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployBackup: input.dokployBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
notificationType: "custom",
|
||||
@@ -757,6 +776,7 @@ export const updateCustomNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployBackup: input.dokployBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
@@ -797,6 +817,7 @@ export const findNotificationById = async (notificationId: string) => {
|
||||
resend: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
mattermost: true,
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
@@ -849,6 +870,8 @@ export const createLarkNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployBackup: input.dokployBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
notificationType: "lark",
|
||||
@@ -880,6 +903,7 @@ export const updateLarkNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployBackup: input.dokployBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
@@ -938,6 +962,7 @@ export const createTeamsNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployBackup: input.dokployBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
@@ -970,6 +995,7 @@ export const updateTeamsNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployBackup: input.dokployBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
@@ -1015,6 +1041,102 @@ export const updateNotificationById = async (
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const createMattermostNotification = async (
|
||||
input: z.infer<typeof apiCreateMattermost>,
|
||||
organizationId: string,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const newMattermost = await tx
|
||||
.insert(mattermost)
|
||||
.values({
|
||||
webhookUrl: input.webhookUrl,
|
||||
channel: input.channel,
|
||||
username: input.username,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newMattermost) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting mattermost",
|
||||
});
|
||||
}
|
||||
|
||||
const newDestination = await tx
|
||||
.insert(notifications)
|
||||
.values({
|
||||
mattermostId: newMattermost.mattermostId,
|
||||
name: input.name,
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployBackup: input.dokployBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
notificationType: "mattermost",
|
||||
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 updateMattermostNotification = async (
|
||||
input: z.infer<typeof apiUpdateMattermost>,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const newDestination = await tx
|
||||
.update(notifications)
|
||||
.set({
|
||||
name: input.name,
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployBackup: input.dokployBackup,
|
||||
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(mattermost)
|
||||
.set({
|
||||
webhookUrl: input.webhookUrl,
|
||||
channel: input.channel,
|
||||
username: input.username,
|
||||
})
|
||||
.where(eq(mattermost.mattermostId, input.mattermostId))
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
return newDestination;
|
||||
});
|
||||
};
|
||||
|
||||
export const createPushoverNotification = async (
|
||||
input: z.infer<typeof apiCreatePushover>,
|
||||
organizationId: string,
|
||||
@@ -1047,6 +1169,7 @@ export const createPushoverNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployBackup: input.dokployBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
@@ -1079,6 +1202,7 @@ export const updatePushoverNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployBackup: input.dokployBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
type apiCreateProject,
|
||||
applications,
|
||||
libsql,
|
||||
mariadb,
|
||||
mongo,
|
||||
mysql,
|
||||
@@ -53,12 +54,13 @@ export const findProjectById = async (projectId: string) => {
|
||||
environments: {
|
||||
with: {
|
||||
applications: true,
|
||||
compose: true,
|
||||
libsql: true,
|
||||
mariadb: true,
|
||||
mongo: true,
|
||||
mysql: true,
|
||||
postgres: true,
|
||||
redis: true,
|
||||
compose: true,
|
||||
},
|
||||
},
|
||||
projectTags: {
|
||||
@@ -109,6 +111,9 @@ export const validUniqueServerAppName = async (appName: string) => {
|
||||
applications: {
|
||||
where: eq(applications.appName, appName),
|
||||
},
|
||||
libsql: {
|
||||
where: eq(libsql.appName, appName),
|
||||
},
|
||||
mariadb: {
|
||||
where: eq(mariadb.appName, appName),
|
||||
},
|
||||
@@ -131,6 +136,7 @@ export const validUniqueServerAppName = async (appName: string) => {
|
||||
const nonEmptyProjects = query.filter(
|
||||
(project) =>
|
||||
project.applications.length > 0 ||
|
||||
project.libsql.length > 0 ||
|
||||
project.mariadb.length > 0 ||
|
||||
project.mongo.length > 0 ||
|
||||
project.mysql.length > 0 ||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { auditLog } from "@dokploy/server/db/schema";
|
||||
import type { AuditAction, AuditResourceType } from "@dokploy/server/db/schema";
|
||||
import { auditLog } from "@dokploy/server/db/schema";
|
||||
import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key";
|
||||
import { and, desc, eq, gte, ilike, lte } from "drizzle-orm";
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
type apiCreateServer,
|
||||
member,
|
||||
organization,
|
||||
server,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
|
||||
export type Server = typeof server.$inferSelect;
|
||||
@@ -80,11 +82,12 @@ export const haveActiveServices = async (serverId: string) => {
|
||||
with: {
|
||||
applications: true,
|
||||
compose: true,
|
||||
redis: true,
|
||||
libsql: true,
|
||||
mariadb: true,
|
||||
mongo: true,
|
||||
mysql: true,
|
||||
postgres: true,
|
||||
redis: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -95,11 +98,12 @@ export const haveActiveServices = async (serverId: string) => {
|
||||
const total =
|
||||
currentServer?.applications?.length +
|
||||
currentServer?.compose?.length +
|
||||
currentServer?.redis?.length +
|
||||
currentServer?.libsql?.length +
|
||||
currentServer?.mariadb?.length +
|
||||
currentServer?.mongo?.length +
|
||||
currentServer?.mysql?.length +
|
||||
currentServer?.postgres?.length;
|
||||
currentServer?.postgres?.length +
|
||||
currentServer?.redis?.length;
|
||||
|
||||
if (total === 0) {
|
||||
return false;
|
||||
@@ -128,3 +132,37 @@ export const getAllServers = async () => {
|
||||
const servers = await db.query.server.findMany();
|
||||
return servers;
|
||||
};
|
||||
|
||||
export const getAccessibleServerIds = async (session: {
|
||||
userId: string;
|
||||
activeOrganizationId: string;
|
||||
}): Promise<Set<string>> => {
|
||||
const { userId, activeOrganizationId } = session;
|
||||
|
||||
const allOrgServers = await db.query.server.findMany({
|
||||
where: eq(server.organizationId, activeOrganizationId),
|
||||
columns: {
|
||||
serverId: true,
|
||||
},
|
||||
});
|
||||
|
||||
const memberRecord = await db.query.member.findFirst({
|
||||
where: and(
|
||||
eq(member.userId, userId),
|
||||
eq(member.organizationId, activeOrganizationId),
|
||||
),
|
||||
columns: { accessedServers: true, role: true },
|
||||
});
|
||||
|
||||
if (memberRecord?.role === "owner" || memberRecord?.role === "admin") {
|
||||
return new Set(allOrgServers.map((s) => s.serverId));
|
||||
}
|
||||
|
||||
const licensed = await hasValidLicense(activeOrganizationId);
|
||||
|
||||
if (!licensed) {
|
||||
return new Set(allOrgServers.map((s) => s.serverId));
|
||||
}
|
||||
|
||||
return new Set(memberRecord?.accessedServers ?? []);
|
||||
};
|
||||
|
||||
@@ -383,12 +383,12 @@ export const readPorts = async (
|
||||
const seenPorts = new Set<string>();
|
||||
for (const key in parsedResult) {
|
||||
if (Object.hasOwn(parsedResult, key)) {
|
||||
const containerPortMapppings = parsedResult[key];
|
||||
const containerPortMappings = parsedResult[key];
|
||||
const protocol = key.split("/")[1];
|
||||
const targetPort = Number.parseInt(key.split("/")[0] ?? "0", 10);
|
||||
|
||||
// Take only the first mapping to avoid duplicates (IPv4 and IPv6)
|
||||
const firstMapping = containerPortMapppings[0];
|
||||
const firstMapping = containerPortMappings[0];
|
||||
if (firstMapping) {
|
||||
const publishedPort = Number.parseInt(firstMapping.HostPort, 10);
|
||||
const portKey = `${targetPort}-${publishedPort}-${protocol}`;
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { apikey, member, user } from "@dokploy/server/db/schema";
|
||||
import {
|
||||
account,
|
||||
apikey,
|
||||
invitation,
|
||||
member,
|
||||
user,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import * as bcrypt from "bcrypt";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { auth } from "../lib/auth";
|
||||
|
||||
@@ -89,7 +96,7 @@ export const canPerformAccessService = async (
|
||||
return false;
|
||||
};
|
||||
|
||||
export const canPeformDeleteService = async (
|
||||
export const canPerformDeleteService = async (
|
||||
userId: string,
|
||||
serviceId: string,
|
||||
organizationId: string,
|
||||
@@ -215,7 +222,7 @@ export const checkServiceAccess = async (
|
||||
);
|
||||
break;
|
||||
case "delete":
|
||||
hasPermission = await canPeformDeleteService(
|
||||
hasPermission = await canPerformDeleteService(
|
||||
userId,
|
||||
serviceId,
|
||||
organizationId,
|
||||
@@ -389,6 +396,93 @@ export const findMemberById = async (
|
||||
return result;
|
||||
};
|
||||
|
||||
export const createOrganizationUserWithCredentials = async ({
|
||||
organizationId,
|
||||
email,
|
||||
password,
|
||||
role,
|
||||
}: {
|
||||
organizationId: string;
|
||||
email: string;
|
||||
password: string;
|
||||
role: string;
|
||||
}) => {
|
||||
const normalizedEmail = email.trim().toLowerCase();
|
||||
const now = new Date();
|
||||
|
||||
return await db.transaction(async (tx) => {
|
||||
const existingUser = await tx.query.user.findFirst({
|
||||
where: eq(user.email, normalizedEmail),
|
||||
columns: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message:
|
||||
"This email already has an account. Use the invitation link flow for existing users.",
|
||||
});
|
||||
}
|
||||
|
||||
const createdUser = await tx
|
||||
.insert(user)
|
||||
.values({
|
||||
email: normalizedEmail,
|
||||
emailVerified: false,
|
||||
updatedAt: now,
|
||||
})
|
||||
.returning({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
})
|
||||
.then((res) => res[0]);
|
||||
|
||||
if (!createdUser) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "Failed to create user",
|
||||
});
|
||||
}
|
||||
|
||||
await tx.insert(account).values({
|
||||
userId: createdUser.id,
|
||||
providerId: "credential",
|
||||
password: bcrypt.hashSync(password, 10),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
|
||||
await tx.insert(member).values({
|
||||
organizationId,
|
||||
userId: createdUser.id,
|
||||
role,
|
||||
createdAt: now,
|
||||
isDefault: true,
|
||||
});
|
||||
|
||||
await tx
|
||||
.update(invitation)
|
||||
.set({
|
||||
status: "canceled",
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(invitation.organizationId, organizationId),
|
||||
eq(invitation.email, normalizedEmail),
|
||||
eq(invitation.status, "pending"),
|
||||
),
|
||||
);
|
||||
|
||||
return {
|
||||
userId: createdUser.id,
|
||||
email: createdUser.email,
|
||||
role,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const updateUser = async (userId: string, userData: Partial<User>) => {
|
||||
// Validate email if it's being updated
|
||||
if (userData.email !== undefined) {
|
||||
|
||||
@@ -75,6 +75,15 @@ export const findVolumeBackupById = async (volumeBackupId: string) => {
|
||||
},
|
||||
},
|
||||
},
|
||||
libsql: {
|
||||
with: {
|
||||
environment: {
|
||||
with: {
|
||||
project: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
destination: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -115,9 +115,20 @@ SYS_ARCH=$(uname -m)
|
||||
CURRENT_USER=$USER
|
||||
|
||||
echo "Installing requirements for: OS: $OS_TYPE"
|
||||
if [ $EUID != 0 ]; then
|
||||
echo "Please run this script as root or with sudo ❌"
|
||||
exit
|
||||
|
||||
# Auto-detect sudo requirement
|
||||
if [ "$EUID" -eq 0 ]; then
|
||||
SUDO_CMD=""
|
||||
echo "Running as root"
|
||||
else
|
||||
if sudo -n true 2>/dev/null; then
|
||||
SUDO_CMD="sudo"
|
||||
echo "Running as $CURRENT_USER with sudo privileges"
|
||||
else
|
||||
echo "Error: Non-root user requires passwordless sudo access. ❌"
|
||||
echo "Configure with: echo '$CURRENT_USER ALL=(ALL) NOPASSWD:ALL' | sudo tee /etc/sudoers.d/$CURRENT_USER"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if the OS is manjaro, if so, change it to arch
|
||||
@@ -152,7 +163,7 @@ else
|
||||
fi
|
||||
|
||||
if [ "$OS_TYPE" = 'amzn' ]; then
|
||||
dnf install -y findutils >/dev/null
|
||||
$SUDO_CMD dnf install -y findutils >/dev/null
|
||||
fi
|
||||
|
||||
case "$OS_TYPE" in
|
||||
@@ -218,6 +229,9 @@ ${installBuildpacks()}
|
||||
|
||||
echo -e "13. Installing Railpack"
|
||||
${installRailpack()}
|
||||
|
||||
echo -e "14. Configuring permissions"
|
||||
${setupPermissions()}
|
||||
`
|
||||
: `
|
||||
echo -e "2. Installing Docker. "
|
||||
@@ -235,6 +249,9 @@ ${installBuildpacks()}
|
||||
|
||||
echo -e "6. Installing Railpack"
|
||||
${installRailpack()}
|
||||
|
||||
echo -e "7. Configuring permissions"
|
||||
${setupPermissions()}
|
||||
`
|
||||
}
|
||||
`;
|
||||
@@ -352,16 +369,18 @@ const setupMainDirectory = () => `
|
||||
echo "/etc/dokploy already exists ✅"
|
||||
else
|
||||
# Create the /etc/dokploy directory
|
||||
mkdir -p /etc/dokploy
|
||||
chmod 777 /etc/dokploy
|
||||
|
||||
$SUDO_CMD mkdir -p /etc/dokploy
|
||||
echo "Directory /etc/dokploy created ✅"
|
||||
fi
|
||||
# Ensure the current user owns the directory
|
||||
if [ -n "$SUDO_CMD" ]; then
|
||||
$SUDO_CMD chown -R $CURRENT_USER:$CURRENT_USER /etc/dokploy
|
||||
fi
|
||||
`;
|
||||
|
||||
export const setupSwarm = () => `
|
||||
# Check if the node is already part of a Docker Swarm
|
||||
if docker info | grep -q 'Swarm: active'; then
|
||||
if $SUDO_CMD docker info | grep -q 'Swarm: active'; then
|
||||
echo "Already part of a Docker Swarm ✅"
|
||||
else
|
||||
# Get IP address
|
||||
@@ -411,18 +430,18 @@ export const setupSwarm = () => `
|
||||
echo "Advertise address: \$advertise_addr"
|
||||
|
||||
# Initialize Docker Swarm
|
||||
docker swarm init --advertise-addr \$advertise_addr
|
||||
$SUDO_CMD docker swarm init --advertise-addr \$advertise_addr
|
||||
echo "Swarm initialized ✅"
|
||||
fi
|
||||
`;
|
||||
|
||||
const setupNetwork = () => `
|
||||
# Check if the dokploy-network already exists
|
||||
if docker network ls | grep -q 'dokploy-network'; then
|
||||
if $SUDO_CMD docker network ls | grep -q 'dokploy-network'; then
|
||||
echo "Network dokploy-network already exists ✅"
|
||||
else
|
||||
# Create the dokploy-network if it doesn't exist
|
||||
if docker network create --driver overlay --attachable dokploy-network; then
|
||||
if $SUDO_CMD docker network create --driver overlay --attachable dokploy-network; then
|
||||
echo "Network created ✅"
|
||||
else
|
||||
echo "Failed to create dokploy-network ❌" >&2
|
||||
@@ -447,33 +466,34 @@ const installUtilities = () => `
|
||||
|
||||
case "$OS_TYPE" in
|
||||
arch)
|
||||
pacman -Sy --noconfirm --needed curl wget git git-lfs jq openssl >/dev/null || true
|
||||
$SUDO_CMD 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 git-lfs jq openssl sudo unzip tar >/dev/null
|
||||
$SUDO_CMD sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories
|
||||
$SUDO_CMD apk update >/dev/null
|
||||
$SUDO_CMD 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 git-lfs jq openssl >/dev/null
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
$SUDO_CMD apt-get update -y >/dev/null
|
||||
$SUDO_CMD apt-get install -y unzip curl wget git git-lfs jq openssl >/dev/null
|
||||
;;
|
||||
centos | fedora | rhel | ol | rocky | almalinux | opencloudos | amzn)
|
||||
if [ "$OS_TYPE" = "amzn" ]; then
|
||||
dnf install -y wget git git-lfs jq openssl >/dev/null
|
||||
$SUDO_CMD 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
|
||||
$SUDO_CMD yum install -y dnf >/dev/null
|
||||
fi
|
||||
if ! command -v curl >/dev/null; then
|
||||
dnf install -y curl >/dev/null
|
||||
$SUDO_CMD dnf install -y curl >/dev/null
|
||||
fi
|
||||
dnf install -y wget git git-lfs jq openssl unzip >/dev/null
|
||||
$SUDO_CMD 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 git-lfs jq openssl >/dev/null
|
||||
$SUDO_CMD zypper refresh >/dev/null
|
||||
$SUDO_CMD 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."
|
||||
@@ -500,41 +520,41 @@ if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker is not installed. Installing Docker. It may take a while."
|
||||
case "$OS_TYPE" in
|
||||
"almalinux" | "rocky" | "centos" | "rhel" | "ol")
|
||||
dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo >/dev/null 2>&1
|
||||
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1
|
||||
$SUDO_CMD dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo >/dev/null 2>&1
|
||||
$SUDO_CMD dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
|
||||
exit 1
|
||||
fi
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
$SUDO_CMD systemctl start docker >/dev/null 2>&1
|
||||
$SUDO_CMD systemctl enable docker >/dev/null 2>&1
|
||||
;;
|
||||
"opencloudos")
|
||||
# Special handling for OpenCloud OS
|
||||
echo " - Installing Docker for OpenCloud OS..."
|
||||
dnf install -y docker >/dev/null 2>&1
|
||||
$SUDO_CMD dnf install -y docker >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
# Remove --live-restore parameter from Docker configuration if it exists
|
||||
if [ -f "/etc/sysconfig/docker" ]; then
|
||||
echo " - Removing --live-restore parameter from Docker configuration..."
|
||||
sed -i 's/--live-restore[^[:space:]]*//' /etc/sysconfig/docker >/dev/null 2>&1
|
||||
sed -i 's/--live-restore//' /etc/sysconfig/docker >/dev/null 2>&1
|
||||
$SUDO_CMD sed -i 's/--live-restore[^[:space:]]*//' /etc/sysconfig/docker >/dev/null 2>&1
|
||||
$SUDO_CMD sed -i 's/--live-restore//' /etc/sysconfig/docker >/dev/null 2>&1
|
||||
# Clean up any double spaces that might be left
|
||||
sed -i 's/ */ /g' /etc/sysconfig/docker >/dev/null 2>&1
|
||||
$SUDO_CMD sed -i 's/ */ /g' /etc/sysconfig/docker >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
|
||||
$SUDO_CMD systemctl enable docker >/dev/null 2>&1
|
||||
$SUDO_CMD systemctl start docker >/dev/null 2>&1
|
||||
echo " - Docker configured for OpenCloud OS"
|
||||
;;
|
||||
"alpine")
|
||||
apk add docker docker-cli-compose >/dev/null 2>&1
|
||||
rc-update add docker default >/dev/null 2>&1
|
||||
service docker start >/dev/null 2>&1
|
||||
$SUDO_CMD apk add docker docker-cli-compose >/dev/null 2>&1
|
||||
$SUDO_CMD rc-update add docker default >/dev/null 2>&1
|
||||
$SUDO_CMD service docker start >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Failed to install Docker with apk. Try to install it manually."
|
||||
echo " Please visit https://wiki.alpinelinux.org/wiki/Docker for more information."
|
||||
@@ -542,8 +562,8 @@ if ! [ -x "$(command -v docker)" ]; then
|
||||
fi
|
||||
;;
|
||||
"arch")
|
||||
pacman -Sy docker docker-compose --noconfirm >/dev/null 2>&1
|
||||
systemctl enable docker.service >/dev/null 2>&1
|
||||
$SUDO_CMD pacman -Sy docker docker-compose --noconfirm >/dev/null 2>&1
|
||||
$SUDO_CMD systemctl enable docker.service >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Failed to install Docker with pacman. Try to install it manually."
|
||||
echo " Please visit https://wiki.archlinux.org/title/docker for more information."
|
||||
@@ -551,13 +571,13 @@ if ! [ -x "$(command -v docker)" ]; then
|
||||
fi
|
||||
;;
|
||||
"amzn")
|
||||
dnf install docker -y >/dev/null 2>&1
|
||||
$SUDO_CMD dnf install docker -y >/dev/null 2>&1
|
||||
DOCKER_CONFIG=/usr/local/lib/docker
|
||||
mkdir -p $DOCKER_CONFIG/cli-plugins >/dev/null 2>&1
|
||||
curl -sL https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1
|
||||
chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
$SUDO_CMD mkdir -p $DOCKER_CONFIG/cli-plugins >/dev/null 2>&1
|
||||
$SUDO_CMD curl -sL https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1
|
||||
$SUDO_CMD chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1
|
||||
$SUDO_CMD systemctl start docker >/dev/null 2>&1
|
||||
$SUDO_CMD systemctl enable docker >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Failed to install Docker with dnf. Try to install it manually."
|
||||
echo " Please visit https://www.cyberciti.biz/faq/how-to-install-docker-on-amazon-linux-2/ for more information."
|
||||
@@ -567,18 +587,18 @@ if ! [ -x "$(command -v docker)" ]; then
|
||||
"fedora")
|
||||
if [ -x "$(command -v dnf5)" ]; then
|
||||
# dnf5 is available
|
||||
dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/fedora/docker-ce.repo --overwrite >/dev/null 2>&1
|
||||
$SUDO_CMD dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/fedora/docker-ce.repo --overwrite >/dev/null 2>&1
|
||||
else
|
||||
# dnf5 is not available, use dnf
|
||||
dnf config-manager --add-repo=https://download.docker.com/linux/fedora/docker-ce.repo >/dev/null 2>&1
|
||||
$SUDO_CMD dnf config-manager --add-repo=https://download.docker.com/linux/fedora/docker-ce.repo >/dev/null 2>&1
|
||||
fi
|
||||
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1
|
||||
$SUDO_CMD dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
|
||||
exit 1
|
||||
fi
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
$SUDO_CMD systemctl start docker >/dev/null 2>&1
|
||||
$SUDO_CMD systemctl enable docker >/dev/null 2>&1
|
||||
;;
|
||||
*)
|
||||
if [ "$OS_TYPE" = "ubuntu" ] && [ "$OS_VERSION" = "24.10" ]; then
|
||||
@@ -586,9 +606,9 @@ if ! [ -x "$(command -v docker)" ]; then
|
||||
echo "Please install Docker manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
curl -s https://get.docker.com | sh -s -- --version $DOCKER_VERSION 2>&1
|
||||
curl -s https://get.docker.com | $SUDO_CMD sh -s -- --version $DOCKER_VERSION 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker installation failed."
|
||||
echo " Maybe your OS is not supported?"
|
||||
@@ -597,13 +617,13 @@ if ! [ -x "$(command -v docker)" ]; then
|
||||
fi
|
||||
fi
|
||||
if [ "$OS_TYPE" = "rocky" ]; then
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
$SUDO_CMD systemctl start docker >/dev/null 2>&1
|
||||
$SUDO_CMD systemctl enable docker >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
if [ "$OS_TYPE" = "centos" ]; then
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
$SUDO_CMD systemctl start docker >/dev/null 2>&1
|
||||
$SUDO_CMD systemctl enable docker >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
|
||||
@@ -647,7 +667,7 @@ export const installRClone = () => `
|
||||
if command_exists rclone; then
|
||||
echo "RClone already installed ✅"
|
||||
else
|
||||
curl https://rclone.org/install.sh | sudo bash
|
||||
curl https://rclone.org/install.sh | $SUDO_CMD bash
|
||||
RCLONE_VERSION=$(rclone --version | head -n 1 | awk '{print $2}' | sed 's/^v//')
|
||||
echo "RClone version $RCLONE_VERSION installed ✅"
|
||||
fi
|
||||
@@ -656,19 +676,19 @@ export const installRClone = () => `
|
||||
export const createTraefikInstance = () => {
|
||||
const command = `
|
||||
# Check if dokpyloy-traefik exists
|
||||
if docker service inspect dokploy-traefik > /dev/null 2>&1; then
|
||||
if $SUDO_CMD docker service inspect dokploy-traefik > /dev/null 2>&1; then
|
||||
echo "Migrating Traefik to Standalone..."
|
||||
docker service rm dokploy-traefik
|
||||
$SUDO_CMD docker service rm dokploy-traefik
|
||||
sleep 8
|
||||
echo "Traefik migrated to Standalone ✅"
|
||||
fi
|
||||
|
||||
if docker inspect dokploy-traefik > /dev/null 2>&1; then
|
||||
if $SUDO_CMD docker inspect dokploy-traefik > /dev/null 2>&1; then
|
||||
echo "Traefik already exists ✅"
|
||||
else
|
||||
# Create the dokploy-traefik container
|
||||
TRAEFIK_VERSION=${TRAEFIK_VERSION}
|
||||
docker run -d \
|
||||
$SUDO_CMD docker run -d \
|
||||
--name dokploy-traefik \
|
||||
--restart always \
|
||||
-v /etc/dokploy/traefik/traefik.yml:/etc/traefik/traefik.yml \
|
||||
@@ -679,7 +699,7 @@ export const createTraefikInstance = () => {
|
||||
-p ${TRAEFIK_HTTP3_PORT}:${TRAEFIK_HTTP3_PORT}/udp \
|
||||
traefik:v$TRAEFIK_VERSION
|
||||
|
||||
docker network connect dokploy-network dokploy-traefik;
|
||||
$SUDO_CMD docker network connect dokploy-network dokploy-traefik;
|
||||
echo "Traefik version $TRAEFIK_VERSION installed ✅"
|
||||
fi
|
||||
`;
|
||||
@@ -692,7 +712,7 @@ const installNixpacks = () => `
|
||||
echo "Nixpacks already installed ✅"
|
||||
else
|
||||
export NIXPACKS_VERSION=1.41.0
|
||||
bash -c "$(curl -fsSL https://nixpacks.com/install.sh)"
|
||||
$SUDO_CMD bash -c "$(curl -fsSL https://nixpacks.com/install.sh)"
|
||||
echo "Nixpacks version $NIXPACKS_VERSION installed ✅"
|
||||
fi
|
||||
`;
|
||||
@@ -702,11 +722,28 @@ const installRailpack = () => `
|
||||
echo "Railpack already installed ✅"
|
||||
else
|
||||
export RAILPACK_VERSION=0.15.4
|
||||
bash -c "$(curl -fsSL https://railpack.com/install.sh)"
|
||||
$SUDO_CMD bash -c "$(curl -fsSL https://railpack.com/install.sh)"
|
||||
echo "Railpack version $RAILPACK_VERSION installed ✅"
|
||||
fi
|
||||
`;
|
||||
|
||||
const setupPermissions = () => `
|
||||
# Add user to docker group if not root
|
||||
if [ -n "$SUDO_CMD" ]; then
|
||||
if ! groups $CURRENT_USER | grep -qw docker; then
|
||||
$SUDO_CMD usermod -aG docker $CURRENT_USER
|
||||
echo "User $CURRENT_USER added to docker group ✅"
|
||||
else
|
||||
echo "User $CURRENT_USER already in docker group ✅"
|
||||
fi
|
||||
# Ensure the user owns the dokploy directory
|
||||
$SUDO_CMD chown -R $CURRENT_USER:$CURRENT_USER /etc/dokploy
|
||||
echo "Permissions configured for $CURRENT_USER ✅"
|
||||
else
|
||||
echo "Running as root, no extra permissions needed ✅"
|
||||
fi
|
||||
`;
|
||||
|
||||
const installBuildpacks = () => `
|
||||
SUFFIX=""
|
||||
if [ "$SYS_ARCH" = "aarch64" ] || [ "$SYS_ARCH" = "arm64" ]; then
|
||||
@@ -716,7 +753,7 @@ const installBuildpacks = () => `
|
||||
echo "Buildpacks already installed ✅"
|
||||
else
|
||||
BUILDPACKS_VERSION=0.39.1
|
||||
curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.39.1/pack-v$BUILDPACKS_VERSION-linux$SUFFIX.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
|
||||
curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.39.1/pack-v$BUILDPACKS_VERSION-linux$SUFFIX.tgz" | $SUDO_CMD tar -C /usr/local/bin/ --no-same-owner -xzv pack
|
||||
echo "Buildpacks version $BUILDPACKS_VERSION installed ✅"
|
||||
fi
|
||||
`;
|
||||
|
||||
@@ -79,6 +79,24 @@ export const validateDokployNetwork = () => `
|
||||
fi
|
||||
`;
|
||||
|
||||
export const validateSudoAccess = () => `
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
echo "root true"
|
||||
elif sudo -n true 2>/dev/null; then
|
||||
echo "sudo true"
|
||||
else
|
||||
echo "none false"
|
||||
fi
|
||||
`;
|
||||
|
||||
export const validateDockerGroup = () => `
|
||||
if groups | grep -qw docker; then
|
||||
echo true
|
||||
else
|
||||
echo false
|
||||
fi
|
||||
`;
|
||||
|
||||
export const serverValidate = async (serverId: string) => {
|
||||
const client = new Client();
|
||||
const server = await findServerById(serverId);
|
||||
@@ -118,7 +136,11 @@ export const serverValidate = async (serverId: string) => {
|
||||
isSwarmInstalled=$(${validateSwarm()})
|
||||
isMainDirectoryInstalled=$(${validateMainDirectory()})
|
||||
|
||||
echo "{\\"docker\\": {\\"version\\": \\"$dockerVersion\\", \\"enabled\\": $dockerEnabled}, \\"rclone\\": {\\"version\\": \\"$rcloneVersion\\", \\"enabled\\": $rcloneEnabled}, \\"nixpacks\\": {\\"version\\": \\"$nixpacksVersion\\", \\"enabled\\": $nixpacksEnabled}, \\"buildpacks\\": {\\"version\\": \\"$buildpacksVersion\\", \\"enabled\\": $buildpacksEnabled}, \\"railpack\\": {\\"version\\": \\"$railpackVersion\\", \\"enabled\\": $railpackEnabled}, \\"isDokployNetworkInstalled\\": $isDokployNetworkInstalled, \\"isSwarmInstalled\\": $isSwarmInstalled, \\"isMainDirectoryInstalled\\": $isMainDirectoryInstalled}"
|
||||
sudoAccessResult=$(${validateSudoAccess()})
|
||||
privilegeMode=$(echo $sudoAccessResult | awk '{print $1}')
|
||||
isDockerGroupMember=$(${validateDockerGroup()})
|
||||
|
||||
echo "{\\"docker\\": {\\"version\\": \\"$dockerVersion\\", \\"enabled\\": $dockerEnabled}, \\"rclone\\": {\\"version\\": \\"$rcloneVersion\\", \\"enabled\\": $rcloneEnabled}, \\"nixpacks\\": {\\"version\\": \\"$nixpacksVersion\\", \\"enabled\\": $nixpacksEnabled}, \\"buildpacks\\": {\\"version\\": \\"$buildpacksVersion\\", \\"enabled\\": $buildpacksEnabled}, \\"railpack\\": {\\"version\\": \\"$railpackVersion\\", \\"enabled\\": $railpackEnabled}, \\"isDokployNetworkInstalled\\": $isDokployNetworkInstalled, \\"isSwarmInstalled\\": $isSwarmInstalled, \\"isMainDirectoryInstalled\\": $isMainDirectoryInstalled, \\"privilegeMode\\": \\"$privilegeMode\\", \\"dockerGroupMember\\": $isDockerGroupMember}"
|
||||
`;
|
||||
client.exec(bashCommand, (err, stream) => {
|
||||
if (err) {
|
||||
|
||||
@@ -3,13 +3,13 @@ import { docker } from "../constants";
|
||||
export const initializeSwarm = async () => {
|
||||
const swarmInitialized = await dockerSwarmInitialized();
|
||||
if (swarmInitialized) {
|
||||
console.log("Swarm is already initilized");
|
||||
console.log("Swarm is already initialized");
|
||||
} else {
|
||||
await docker.swarmInit({
|
||||
AdvertiseAddr: "127.0.0.1",
|
||||
ListenAddr: "0.0.0.0",
|
||||
});
|
||||
console.log("Swarm was initilized");
|
||||
console.log("Swarm was initialized");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -26,14 +26,14 @@ export const dockerSwarmInitialized = async () => {
|
||||
export const initializeNetwork = async () => {
|
||||
const networkInitialized = await dockerNetworkInitialized();
|
||||
if (networkInitialized) {
|
||||
console.log("Network is already initilized");
|
||||
console.log("Network is already initialized");
|
||||
} else {
|
||||
docker.createNetwork({
|
||||
Attachable: true,
|
||||
Name: "dokploy-network",
|
||||
Driver: "overlay",
|
||||
});
|
||||
console.log("Network was initilized");
|
||||
console.log("Network was initialized");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -8,7 +8,12 @@ import { findEnvironmentById } from "@dokploy/server/services/environment";
|
||||
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 {
|
||||
getBackupCommand,
|
||||
getBackupTimestamp,
|
||||
getS3Credentials,
|
||||
normalizeS3Path,
|
||||
} from "./utils";
|
||||
|
||||
export const runComposeBackup = async (
|
||||
compose: Compose,
|
||||
@@ -19,7 +24,7 @@ export const runComposeBackup = async (
|
||||
const project = await findProjectById(environment.projectId);
|
||||
const { prefix, databaseType, serviceName } = backup;
|
||||
const destination = backup.destination;
|
||||
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
||||
const backupFileName = `${getBackupTimestamp()}.${databaseType === "mongo" ? "bson" : "sql"}.gz`;
|
||||
const s3AppName = serviceName ? `${appName}_${serviceName}` : appName;
|
||||
const bucketDestination = `${s3AppName}/${normalizeS3Path(prefix)}${backupFileName}`;
|
||||
const deployment = await createDeploymentBackup({
|
||||
|
||||
@@ -75,6 +75,7 @@ export const initCronJobs = async () => {
|
||||
mariadb: true,
|
||||
mysql: true,
|
||||
mongo: true,
|
||||
libsql: true,
|
||||
user: true,
|
||||
compose: true,
|
||||
},
|
||||
@@ -116,7 +117,8 @@ const getServiceAppName = (backup: BackupSchedule): string => {
|
||||
backup.postgres?.appName ||
|
||||
backup.mysql?.appName ||
|
||||
backup.mariadb?.appName ||
|
||||
backup.mongo?.appName;
|
||||
backup.mongo?.appName ||
|
||||
backup.libsql?.appName;
|
||||
return serviceAppName || backup.appName;
|
||||
};
|
||||
|
||||
@@ -133,8 +135,8 @@ export const keepLatestNBackups = async (
|
||||
const appName = getServiceAppName(backup);
|
||||
const backupFilesPath = `:s3:${backup.destination.bucket}/${appName}/${normalizeS3Path(backup.prefix)}`;
|
||||
|
||||
// --include "*.sql.gz" or "*.zip" ensures nothing else other than the dokploy backup files are touched by rclone
|
||||
const rcloneList = `rclone lsf ${rcloneFlags.join(" ")} --include "*${backup.databaseType === "web-server" ? ".zip" : ".sql.gz"}" ${backupFilesPath}`;
|
||||
// --include "*.bson.gz" or "*.sql.gz" or "*.zip" ensures nothing else other than the dokploy backup files are touched by rclone
|
||||
const rcloneList = `rclone lsf ${rcloneFlags.join(" ")} --include "*${backup.databaseType === "web-server" ? ".zip" : ".{sql.gz,bson.gz}"}" ${backupFilesPath}`;
|
||||
// when we pipe the above command with this one, we only get the list of files we want to delete
|
||||
const sortAndPickUnwantedBackups = `sort -r | tail -n +$((${backup.keepLatestCount}+1)) | xargs -I{}`;
|
||||
// this command deletes the files
|
||||
|
||||
80
packages/server/src/utils/backups/libsql.ts
Normal file
80
packages/server/src/utils/backups/libsql.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
||||
import {
|
||||
createDeploymentBackup,
|
||||
updateDeploymentStatus,
|
||||
} from "@dokploy/server/services/deployment";
|
||||
import { findEnvironmentById } from "@dokploy/server/services/environment";
|
||||
import type { Libsql } from "@dokploy/server/services/libsql";
|
||||
import { findProjectById } from "@dokploy/server/services/project";
|
||||
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import {
|
||||
getBackupCommand,
|
||||
getBackupTimestamp,
|
||||
getS3Credentials,
|
||||
normalizeS3Path,
|
||||
} from "./utils";
|
||||
|
||||
export const runLibsqlBackup = async (
|
||||
libsql: Libsql,
|
||||
backup: BackupSchedule,
|
||||
) => {
|
||||
const { name, environmentId, appName } = libsql;
|
||||
const environment = await findEnvironmentById(environmentId);
|
||||
const project = await findProjectById(environment.projectId);
|
||||
|
||||
const deployment = await createDeploymentBackup({
|
||||
backupId: backup.backupId,
|
||||
title: "Initializing Backup",
|
||||
description: "Initializing Backup",
|
||||
});
|
||||
const { prefix } = backup;
|
||||
const destination = backup.destination;
|
||||
const backupFileName = `${getBackupTimestamp()}.sql.gz`;
|
||||
const bucketDestination = `${appName}/${normalizeS3Path(prefix)}${backupFileName}`;
|
||||
try {
|
||||
const rcloneFlags = getS3Credentials(destination);
|
||||
const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`;
|
||||
|
||||
const rcloneCommand = `rclone rcat ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
|
||||
|
||||
const backupCommand = getBackupCommand(
|
||||
backup,
|
||||
rcloneCommand,
|
||||
deployment.logPath,
|
||||
);
|
||||
if (libsql.serverId) {
|
||||
await execAsyncRemote(libsql.serverId, backupCommand);
|
||||
} else {
|
||||
await execAsync(backupCommand, {
|
||||
shell: "/bin/bash",
|
||||
});
|
||||
}
|
||||
|
||||
await sendDatabaseBackupNotifications({
|
||||
applicationName: name,
|
||||
projectName: project.name,
|
||||
databaseType: "libsql",
|
||||
type: "success",
|
||||
organizationId: project.organizationId,
|
||||
databaseName: backup.database,
|
||||
});
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
} catch (error) {
|
||||
await sendDatabaseBackupNotifications({
|
||||
applicationName: name,
|
||||
projectName: project.name,
|
||||
databaseType: "libsql",
|
||||
type: "error",
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error message not provided",
|
||||
organizationId: project.organizationId,
|
||||
databaseName: backup.database,
|
||||
});
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -8,7 +8,12 @@ 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 {
|
||||
getBackupCommand,
|
||||
getBackupTimestamp,
|
||||
getS3Credentials,
|
||||
normalizeS3Path,
|
||||
} from "./utils";
|
||||
|
||||
export const runMariadbBackup = async (
|
||||
mariadb: Mariadb,
|
||||
@@ -19,7 +24,7 @@ export const runMariadbBackup = async (
|
||||
const project = await findProjectById(environment.projectId);
|
||||
const { prefix } = backup;
|
||||
const destination = backup.destination;
|
||||
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
||||
const backupFileName = `${getBackupTimestamp()}.sql.gz`;
|
||||
const bucketDestination = `${appName}/${normalizeS3Path(prefix)}${backupFileName}`;
|
||||
const deployment = await createDeploymentBackup({
|
||||
backupId: backup.backupId,
|
||||
|
||||
@@ -8,7 +8,12 @@ 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 {
|
||||
getBackupCommand,
|
||||
getBackupTimestamp,
|
||||
getS3Credentials,
|
||||
normalizeS3Path,
|
||||
} from "./utils";
|
||||
|
||||
export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
|
||||
const { environmentId, name, appName } = mongo;
|
||||
@@ -16,7 +21,7 @@ export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
|
||||
const project = await findProjectById(environment.projectId);
|
||||
const { prefix } = backup;
|
||||
const destination = backup.destination;
|
||||
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
||||
const backupFileName = `${getBackupTimestamp()}.bson.gz`;
|
||||
const bucketDestination = `${appName}/${normalizeS3Path(prefix)}${backupFileName}`;
|
||||
const deployment = await createDeploymentBackup({
|
||||
backupId: backup.backupId,
|
||||
|
||||
@@ -8,7 +8,12 @@ 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 {
|
||||
getBackupCommand,
|
||||
getBackupTimestamp,
|
||||
getS3Credentials,
|
||||
normalizeS3Path,
|
||||
} from "./utils";
|
||||
|
||||
export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => {
|
||||
const { environmentId, name, appName } = mysql;
|
||||
@@ -16,7 +21,7 @@ export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => {
|
||||
const project = await findProjectById(environment.projectId);
|
||||
const { prefix } = backup;
|
||||
const destination = backup.destination;
|
||||
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
||||
const backupFileName = `${getBackupTimestamp()}.sql.gz`;
|
||||
const bucketDestination = `${appName}/${normalizeS3Path(prefix)}${backupFileName}`;
|
||||
const deployment = await createDeploymentBackup({
|
||||
backupId: backup.backupId,
|
||||
|
||||
@@ -8,7 +8,12 @@ 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 {
|
||||
getBackupCommand,
|
||||
getBackupTimestamp,
|
||||
getS3Credentials,
|
||||
normalizeS3Path,
|
||||
} from "./utils";
|
||||
|
||||
export const runPostgresBackup = async (
|
||||
postgres: Postgres,
|
||||
@@ -25,7 +30,7 @@ export const runPostgresBackup = async (
|
||||
});
|
||||
const { prefix } = backup;
|
||||
const destination = backup.destination;
|
||||
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
||||
const backupFileName = `${getBackupTimestamp()}.sql.gz`;
|
||||
const bucketDestination = `${appName}/${normalizeS3Path(prefix)}${backupFileName}`;
|
||||
try {
|
||||
const rcloneFlags = getS3Credentials(destination);
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { Destination } from "@dokploy/server/services/destination";
|
||||
import { scheduledJobs, scheduleJob } from "node-schedule";
|
||||
import { keepLatestNBackups } from ".";
|
||||
import { runComposeBackup } from "./compose";
|
||||
import { runLibsqlBackup } from "./libsql";
|
||||
import { runMariadbBackup } from "./mariadb";
|
||||
import { runMongoBackup } from "./mongo";
|
||||
import { runMySqlBackup } from "./mysql";
|
||||
@@ -19,6 +20,7 @@ export const scheduleBackup = (backup: BackupSchedule) => {
|
||||
mysql,
|
||||
mongo,
|
||||
mariadb,
|
||||
libsql,
|
||||
compose,
|
||||
} = backup;
|
||||
scheduleJob(backupId, schedule, async () => {
|
||||
@@ -35,6 +37,9 @@ export const scheduleBackup = (backup: BackupSchedule) => {
|
||||
} else if (databaseType === "mariadb" && mariadb) {
|
||||
await runMariadbBackup(mariadb, backup);
|
||||
await keepLatestNBackups(backup, mariadb.serverId);
|
||||
} else if (databaseType === "libsql" && libsql) {
|
||||
await runLibsqlBackup(libsql, backup);
|
||||
await keepLatestNBackups(backup, libsql.serverId);
|
||||
} else if (databaseType === "web-server") {
|
||||
await runWebServerBackup(backup);
|
||||
await keepLatestNBackups(backup);
|
||||
@@ -51,6 +56,9 @@ export const removeScheduleBackup = (backupId: string) => {
|
||||
currentJob?.cancel();
|
||||
};
|
||||
|
||||
export const getBackupTimestamp = () =>
|
||||
new Date().toISOString().replace(/[:.]/g, "-");
|
||||
|
||||
export const normalizeS3Path = (prefix: string) => {
|
||||
// Trim whitespace and remove leading/trailing slashes
|
||||
const normalizedPrefix = prefix.trim().replace(/^\/+|\/+$/g, "");
|
||||
@@ -74,6 +82,10 @@ export const getS3Credentials = (destination: Destination) => {
|
||||
rcloneFlags.unshift(`--s3-provider="${provider}"`);
|
||||
}
|
||||
|
||||
if (destination.additionalFlags?.length) {
|
||||
rcloneFlags.push(...destination.additionalFlags);
|
||||
}
|
||||
|
||||
return rcloneFlags;
|
||||
};
|
||||
|
||||
@@ -107,6 +119,10 @@ export const getMongoBackupCommand = (
|
||||
return `docker exec -i $CONTAINER_ID bash -c "set -o pipefail; mongodump -d '${database}' -u '${databaseUser}' -p '${databasePassword}' --archive --authenticationDatabase admin --gzip"`;
|
||||
};
|
||||
|
||||
export const getLibsqlBackupCommand = (database: string) => {
|
||||
return `docker exec -i $CONTAINER_ID sh -c "tar cf - -C /var/lib/sqld ${database} | gzip"`;
|
||||
};
|
||||
|
||||
export const getServiceContainerCommand = (appName: string) => {
|
||||
return `docker ps -q --filter "status=running" --filter "label=com.docker.swarm.service.name=${appName}" | head -n 1`;
|
||||
};
|
||||
@@ -123,12 +139,24 @@ export const getComposeContainerCommand = (
|
||||
};
|
||||
|
||||
const getContainerSearchCommand = (backup: BackupSchedule) => {
|
||||
const { backupType, postgres, mysql, mariadb, mongo, compose, serviceName } =
|
||||
backup;
|
||||
const {
|
||||
backupType,
|
||||
postgres,
|
||||
mysql,
|
||||
mariadb,
|
||||
mongo,
|
||||
libsql,
|
||||
compose,
|
||||
serviceName,
|
||||
} = backup;
|
||||
|
||||
if (backupType === "database") {
|
||||
const appName =
|
||||
postgres?.appName || mysql?.appName || mariadb?.appName || mongo?.appName;
|
||||
postgres?.appName ||
|
||||
mysql?.appName ||
|
||||
mariadb?.appName ||
|
||||
mongo?.appName ||
|
||||
libsql?.appName;
|
||||
return getServiceContainerCommand(appName || "");
|
||||
}
|
||||
if (backupType === "compose") {
|
||||
@@ -209,6 +237,12 @@ export const generateBackupCommand = (backup: BackupSchedule) => {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "libsql": {
|
||||
if (backupType === "database") {
|
||||
return getLibsqlBackupCommand(backup.database);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error(`Database type not supported: ${databaseType}`);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { mkdtemp, rm, stat } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { IS_CLOUD, paths } from "@dokploy/server/constants";
|
||||
@@ -9,8 +9,18 @@ import {
|
||||
updateDeploymentStatus,
|
||||
} from "@dokploy/server/services/deployment";
|
||||
import { findDestinationById } from "@dokploy/server/services/destination";
|
||||
import { sendDokployBackupNotifications } from "../notifications/dokploy-backup";
|
||||
import { execAsync } from "../process/execAsync";
|
||||
import { getS3Credentials, normalizeS3Path } from "./utils";
|
||||
import { getBackupTimestamp, getS3Credentials, normalizeS3Path } from "./utils";
|
||||
|
||||
function formatBytes(bytes?: number) {
|
||||
if (bytes === undefined) return "Unknown size";
|
||||
if (bytes === 0) return "0 B";
|
||||
const sizes = ["B", "KB", "MB", "GB", "TB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
const value = bytes / Math.pow(1024, i);
|
||||
return `${value.toFixed(2)} ${sizes[i]} (${bytes} bytes)`;
|
||||
}
|
||||
|
||||
export const runWebServerBackup = async (backup: BackupSchedule) => {
|
||||
if (IS_CLOUD) {
|
||||
@@ -23,11 +33,11 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
|
||||
description: "Web Server Backup",
|
||||
});
|
||||
const writeStream = createWriteStream(deployment.logPath, { flags: "a" });
|
||||
|
||||
let computedBackupSize: number | undefined;
|
||||
try {
|
||||
const destination = await findDestinationById(backup.destinationId);
|
||||
const rcloneFlags = getS3Credentials(destination);
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
const timestamp = getBackupTimestamp();
|
||||
const { BASE_PATH } = paths();
|
||||
const tempDir = await mkdtemp(join(tmpdir(), "dokploy-backup-"));
|
||||
const backupFileName = `webserver-backup-${timestamp}.zip`;
|
||||
@@ -79,11 +89,24 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
|
||||
|
||||
writeStream.write("Zipped database and filesystem\n");
|
||||
|
||||
const uploadCommand = `rclone copyto ${rcloneFlags.join(" ")} "${tempDir}/${backupFileName}" "${s3Path}"`;
|
||||
const zipPath = join(tempDir, backupFileName);
|
||||
try {
|
||||
const { size } = await stat(zipPath);
|
||||
computedBackupSize = size;
|
||||
writeStream.write(`Backup size: ${size} bytes\n`);
|
||||
} catch {
|
||||
// If stat fails, keep undefined
|
||||
}
|
||||
|
||||
const uploadCommand = `rclone copyto ${rcloneFlags.join(" ")} "${zipPath}" "${s3Path}"`;
|
||||
writeStream.write("Running command to upload backup to S3\n");
|
||||
await execAsync(uploadCommand);
|
||||
writeStream.write("Uploaded backup to S3 ✅\n");
|
||||
writeStream.end();
|
||||
await sendDokployBackupNotifications({
|
||||
type: "success",
|
||||
backupSize: formatBytes(computedBackupSize),
|
||||
});
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
return true;
|
||||
} finally {
|
||||
@@ -100,6 +123,12 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
|
||||
error instanceof Error ? error.message : "Unknown error\n",
|
||||
);
|
||||
writeStream.end();
|
||||
await sendDokployBackupNotifications({
|
||||
type: "error",
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error message not provided",
|
||||
backupSize: formatBytes(computedBackupSize),
|
||||
});
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { quote } from "shell-quote";
|
||||
import { writeDomainsToCompose } from "../docker/domain";
|
||||
import {
|
||||
encodeBase64,
|
||||
getEnviromentVariablesObject,
|
||||
getEnvironmentVariablesObject,
|
||||
prepareEnvironmentVariables,
|
||||
} from "../docker/utils";
|
||||
|
||||
@@ -46,17 +46,17 @@ Compose Type: ${composeType} ✅`;
|
||||
set -e
|
||||
{
|
||||
echo "${logBox}";
|
||||
|
||||
|
||||
${newCompose}
|
||||
|
||||
|
||||
${envCommand}
|
||||
|
||||
|
||||
cd "${projectPath}";
|
||||
|
||||
${compose.isolatedDeployment ? `docker network inspect ${compose.appName} >/dev/null 2>&1 || docker network create ${compose.composeType === "stack" ? "--driver overlay" : ""} --attachable ${compose.appName}` : ""}
|
||||
env -i PATH="$PATH" ${exportEnvCommand} docker ${command.split(" ").join(" ")} 2>&1 || { echo "Error: ❌ Docker command failed"; exit 1; }
|
||||
${compose.isolatedDeployment ? `docker network connect ${compose.appName} $(docker ps --filter "name=dokploy-traefik" -q) >/dev/null 2>&1` : ""}
|
||||
|
||||
|
||||
echo "Docker Compose Deployed: ✅";
|
||||
} || {
|
||||
echo "Error: ❌ Script execution failed";
|
||||
@@ -131,7 +131,7 @@ echo "${encodedContent}" | base64 -d > "${envFilePath}";
|
||||
const getExportEnvCommand = (compose: ComposeNested) => {
|
||||
if (compose.composeType !== "stack") return "";
|
||||
|
||||
const envVars = getEnviromentVariablesObject(
|
||||
const envVars = getEnvironmentVariablesObject(
|
||||
compose.env,
|
||||
compose.environment.project.env,
|
||||
compose.environment.env,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
getEnviromentVariablesObject,
|
||||
getEnvironmentVariablesObject,
|
||||
prepareEnvironmentVariablesForShell,
|
||||
} from "@dokploy/server/utils/docker/utils";
|
||||
import { quote } from "shell-quote";
|
||||
@@ -52,7 +52,7 @@ export const getDockerCommand = (application: ApplicationNested) => {
|
||||
commandArgs.push("--build-arg", arg);
|
||||
}
|
||||
|
||||
const secrets = getEnviromentVariablesObject(
|
||||
const secrets = getEnvironmentVariablesObject(
|
||||
buildSecrets,
|
||||
application.environment.project.env,
|
||||
application.environment.env,
|
||||
@@ -86,12 +86,12 @@ export const getDockerCommand = (application: ApplicationNested) => {
|
||||
|
||||
command += `
|
||||
echo "Building ${appName}" ;
|
||||
cd ${dockerContextPath} || {
|
||||
cd ${dockerContextPath} || {
|
||||
echo "❌ The path ${dockerContextPath} does not exist" ;
|
||||
exit 1;
|
||||
}
|
||||
|
||||
${joinedSecrets} docker ${commandArgs.join(" ")} || {
|
||||
${joinedSecrets} docker ${commandArgs.join(" ")} || {
|
||||
echo "❌ Docker build failed" ;
|
||||
exit 1;
|
||||
}
|
||||
|
||||
155
packages/server/src/utils/databases/libsql.ts
Normal file
155
packages/server/src/utils/databases/libsql.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import type { InferResultType } from "@dokploy/server/types/with";
|
||||
import type { CreateServiceOptions, PortConfig } from "dockerode";
|
||||
import {
|
||||
calculateResources,
|
||||
generateBindMounts,
|
||||
generateConfigContainer,
|
||||
generateFileMounts,
|
||||
generateVolumeMounts,
|
||||
prepareEnvironmentVariables,
|
||||
} from "../docker/utils";
|
||||
import { getRemoteDocker } from "../servers/remote-docker";
|
||||
|
||||
export type LibsqlNested = InferResultType<
|
||||
"libsql",
|
||||
{
|
||||
mounts: true;
|
||||
environment: { with: { project: true } };
|
||||
}
|
||||
>;
|
||||
export const buildLibsql = async (libsql: LibsqlNested) => {
|
||||
const {
|
||||
appName,
|
||||
env,
|
||||
externalPort,
|
||||
externalGRPCPort,
|
||||
externalAdminPort,
|
||||
memoryLimit,
|
||||
memoryReservation,
|
||||
databaseUser,
|
||||
databasePassword,
|
||||
sqldNode,
|
||||
sqldPrimaryUrl,
|
||||
cpuLimit,
|
||||
cpuReservation,
|
||||
command,
|
||||
mounts,
|
||||
enableNamespaces,
|
||||
} = libsql;
|
||||
|
||||
const basicAuth = Buffer.from(
|
||||
`${databaseUser}:${databasePassword}`,
|
||||
"utf-8",
|
||||
).toString("base64");
|
||||
|
||||
const defaultLibsqlEnv = `SQLD_NODE="${sqldNode}"\nSQLD_HTTP_AUTH="basic:${basicAuth}"${
|
||||
env ? `\n${env}` : ""
|
||||
}${sqldNode === "replica" ? `\nSQLD_PRIMARY_URL="${sqldPrimaryUrl}"` : ""}`;
|
||||
|
||||
const {
|
||||
HealthCheck,
|
||||
RestartPolicy,
|
||||
Placement,
|
||||
Labels,
|
||||
Mode,
|
||||
RollbackConfig,
|
||||
UpdateConfig,
|
||||
Networks,
|
||||
} = generateConfigContainer(libsql);
|
||||
const resources = calculateResources({
|
||||
memoryLimit,
|
||||
memoryReservation,
|
||||
cpuLimit,
|
||||
cpuReservation,
|
||||
});
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
defaultLibsqlEnv,
|
||||
libsql.environment.project.env,
|
||||
libsql.environment.env,
|
||||
);
|
||||
const volumesMount = generateVolumeMounts(mounts);
|
||||
const bindsMount = generateBindMounts(mounts);
|
||||
const filesMount = generateFileMounts(appName, libsql);
|
||||
|
||||
const docker = await getRemoteDocker(libsql.serverId);
|
||||
|
||||
let finalCommand =
|
||||
command ??
|
||||
"sqld --db-path iku.db --http-listen-addr 0.0.0.0:8080 --grpc-listen-addr 0.0.0.0:5001 --admin-listen-addr 0.0.0.0:5000";
|
||||
if (enableNamespaces) {
|
||||
finalCommand += " --enable-namespaces";
|
||||
}
|
||||
|
||||
const settings: CreateServiceOptions = {
|
||||
Name: appName,
|
||||
TaskTemplate: {
|
||||
ContainerSpec: {
|
||||
HealthCheck,
|
||||
Image: "ghcr.io/tursodatabase/libsql-server:v0.24.32",
|
||||
Env: envVariables,
|
||||
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
|
||||
...(finalCommand
|
||||
? {
|
||||
Command: ["/bin/sh"],
|
||||
Args: ["-c", finalCommand],
|
||||
}
|
||||
: {}),
|
||||
Labels,
|
||||
},
|
||||
Networks,
|
||||
RestartPolicy,
|
||||
Placement,
|
||||
Resources: {
|
||||
...resources,
|
||||
},
|
||||
},
|
||||
Mode,
|
||||
RollbackConfig,
|
||||
EndpointSpec: {
|
||||
Mode: "dnsrr",
|
||||
Ports: [
|
||||
...(externalPort
|
||||
? [
|
||||
{
|
||||
Protocol: "tcp",
|
||||
TargetPort: 8080,
|
||||
PublishedPort: externalPort,
|
||||
PublishMode: "host",
|
||||
} as PortConfig,
|
||||
]
|
||||
: []),
|
||||
...(externalGRPCPort
|
||||
? [
|
||||
{
|
||||
Protocol: "tcp",
|
||||
TargetPort: 5001,
|
||||
PublishedPort: externalGRPCPort,
|
||||
PublishMode: "host",
|
||||
} as PortConfig,
|
||||
]
|
||||
: []),
|
||||
...(externalAdminPort
|
||||
? [
|
||||
{
|
||||
Protocol: "tcp",
|
||||
TargetPort: 5000,
|
||||
PublishedPort: externalAdminPort,
|
||||
PublishMode: "host",
|
||||
} as PortConfig,
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
UpdateConfig,
|
||||
};
|
||||
try {
|
||||
const service = docker.getService(appName);
|
||||
const inspect = await service.inspect();
|
||||
await service.update({
|
||||
version: Number.parseInt(inspect.Version.Index),
|
||||
...settings,
|
||||
});
|
||||
} catch {
|
||||
await docker.createService(settings);
|
||||
}
|
||||
};
|
||||
@@ -74,8 +74,7 @@ export const buildPostgres = async (postgres: PostgresNested) => {
|
||||
Image: dockerImage,
|
||||
Env: envVariables,
|
||||
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
|
||||
...(StopGracePeriod !== null &&
|
||||
StopGracePeriod !== undefined && { StopGracePeriod }),
|
||||
StopGracePeriod: StopGracePeriod ?? 30_000_000_000,
|
||||
...(command && {
|
||||
Command: command.split(" "),
|
||||
}),
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
libsql,
|
||||
mariadb,
|
||||
mongo,
|
||||
mysql,
|
||||
postgres,
|
||||
redis,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { deployLibsql } from "@dokploy/server/services/libsql";
|
||||
import { deployMariadb } from "@dokploy/server/services/mariadb";
|
||||
import { deployMongo } from "@dokploy/server/services/mongo";
|
||||
import { deployMySql } from "@dokploy/server/services/mysql";
|
||||
@@ -15,7 +17,13 @@ import { eq } from "drizzle-orm";
|
||||
import { removeService } from "../docker/utils";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
|
||||
type DatabaseType = "postgres" | "mysql" | "mariadb" | "mongo" | "redis";
|
||||
type DatabaseType =
|
||||
| "libsql"
|
||||
| "mariadb"
|
||||
| "mongo"
|
||||
| "mysql"
|
||||
| "postgres"
|
||||
| "redis";
|
||||
|
||||
export const rebuildDatabase = async (
|
||||
databaseId: string,
|
||||
@@ -41,31 +49,25 @@ export const rebuildDatabase = async (
|
||||
}
|
||||
}
|
||||
|
||||
if (type === "postgres") {
|
||||
await deployPostgres(databaseId);
|
||||
} else if (type === "mysql") {
|
||||
await deployMySql(databaseId);
|
||||
if (type === "libsql") {
|
||||
await deployLibsql(databaseId);
|
||||
} else if (type === "mariadb") {
|
||||
await deployMariadb(databaseId);
|
||||
} else if (type === "mongo") {
|
||||
await deployMongo(databaseId);
|
||||
} else if (type === "mysql") {
|
||||
await deployMySql(databaseId);
|
||||
} else if (type === "postgres") {
|
||||
await deployPostgres(databaseId);
|
||||
} else if (type === "redis") {
|
||||
await deployRedis(databaseId);
|
||||
}
|
||||
};
|
||||
|
||||
const findDatabaseById = async (databaseId: string, type: DatabaseType) => {
|
||||
if (type === "postgres") {
|
||||
return await db.query.postgres.findFirst({
|
||||
where: eq(postgres.postgresId, databaseId),
|
||||
with: {
|
||||
mounts: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (type === "mysql") {
|
||||
return await db.query.mysql.findFirst({
|
||||
where: eq(mysql.mysqlId, databaseId),
|
||||
if (type === "libsql") {
|
||||
return await db.query.libsql.findFirst({
|
||||
where: eq(libsql.libsqlId, databaseId),
|
||||
with: {
|
||||
mounts: true,
|
||||
},
|
||||
@@ -87,6 +89,22 @@ const findDatabaseById = async (databaseId: string, type: DatabaseType) => {
|
||||
},
|
||||
});
|
||||
}
|
||||
if (type === "mysql") {
|
||||
return await db.query.mysql.findFirst({
|
||||
where: eq(mysql.mysqlId, databaseId),
|
||||
with: {
|
||||
mounts: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (type === "postgres") {
|
||||
return await db.query.postgres.findFirst({
|
||||
where: eq(postgres.postgresId, databaseId),
|
||||
with: {
|
||||
mounts: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (type === "redis") {
|
||||
return await db.query.redis.findFirst({
|
||||
where: eq(redis.redisId, databaseId),
|
||||
|
||||
@@ -172,8 +172,12 @@ export const addDomainToCompose = async (
|
||||
);
|
||||
}
|
||||
|
||||
const httpLabels = createDomainLabels(appName, domain, "web");
|
||||
if (https) {
|
||||
const httpLabels = createDomainLabels(
|
||||
appName,
|
||||
domain,
|
||||
domain.customEntrypoint || "web",
|
||||
);
|
||||
if (!domain.customEntrypoint && https) {
|
||||
const httpsLabels = createDomainLabels(appName, domain, "websecure");
|
||||
httpLabels.push(...httpsLabels);
|
||||
}
|
||||
@@ -251,11 +255,12 @@ export const writeComposeFile = async (
|
||||
export const createDomainLabels = (
|
||||
appName: string,
|
||||
domain: Domain,
|
||||
entrypoint: "web" | "websecure",
|
||||
entrypoint: string,
|
||||
) => {
|
||||
const {
|
||||
host,
|
||||
port,
|
||||
customEntrypoint,
|
||||
https,
|
||||
uniqueConfigKey,
|
||||
certificateType,
|
||||
@@ -274,34 +279,45 @@ export const createDomainLabels = (
|
||||
|
||||
// Collect middlewares for this router
|
||||
const middlewares: string[] = [];
|
||||
const isRedirectRouter = entrypoint === "web" && https && !customEntrypoint;
|
||||
|
||||
// Add HTTPS redirect for web entrypoint (must be first)
|
||||
if (entrypoint === "web" && https) {
|
||||
// Web router with HTTPS only needs redirect — all other middlewares
|
||||
// run on the websecure router where the request actually lands.
|
||||
if (isRedirectRouter) {
|
||||
middlewares.push("redirect-to-https@file");
|
||||
}
|
||||
|
||||
// Add stripPath middleware if needed
|
||||
if (stripPath && path && path !== "/") {
|
||||
const middlewareName = `stripprefix-${appName}-${uniqueConfigKey}`;
|
||||
// Only define middleware once (on web entrypoint)
|
||||
if (entrypoint === "web") {
|
||||
// Define middleware on web (or custom) entrypoint so Traefik registers it
|
||||
if (entrypoint === "web" || customEntrypoint) {
|
||||
labels.push(
|
||||
`traefik.http.middlewares.${middlewareName}.stripprefix.prefixes=${path}`,
|
||||
);
|
||||
}
|
||||
middlewares.push(middlewareName);
|
||||
if (!isRedirectRouter) {
|
||||
middlewares.push(middlewareName);
|
||||
}
|
||||
}
|
||||
|
||||
// Add internalPath middleware if needed
|
||||
if (internalPath && internalPath !== "/" && internalPath.startsWith("/")) {
|
||||
const middlewareName = `addprefix-${appName}-${uniqueConfigKey}`;
|
||||
// Only define middleware once (on web entrypoint)
|
||||
if (entrypoint === "web") {
|
||||
// Define middleware on web (or custom) entrypoint so Traefik registers it
|
||||
if (entrypoint === "web" || customEntrypoint) {
|
||||
labels.push(
|
||||
`traefik.http.middlewares.${middlewareName}.addprefix.prefix=${internalPath}`,
|
||||
);
|
||||
}
|
||||
middlewares.push(middlewareName);
|
||||
if (!isRedirectRouter) {
|
||||
middlewares.push(middlewareName);
|
||||
}
|
||||
}
|
||||
|
||||
// Add custom middlewares (skip for redirect-only router)
|
||||
if (!isRedirectRouter && domain.middlewares?.length) {
|
||||
middlewares.push(...domain.middlewares);
|
||||
}
|
||||
|
||||
// Apply middlewares to router if any exist
|
||||
@@ -312,7 +328,7 @@ export const createDomainLabels = (
|
||||
}
|
||||
|
||||
// Add TLS configuration for websecure
|
||||
if (entrypoint === "websecure") {
|
||||
if (entrypoint === "websecure" || (customEntrypoint && https)) {
|
||||
if (certificateType === "letsencrypt") {
|
||||
labels.push(
|
||||
`traefik.http.routers.${routerName}.tls.certresolver=letsencrypt`,
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { ContainerInfo, ResourceRequirements } from "dockerode";
|
||||
import { parse } from "dotenv";
|
||||
import { quote } from "shell-quote";
|
||||
import type { ApplicationNested } from "../builders";
|
||||
import type { LibsqlNested } from "../databases/libsql";
|
||||
import type { MariadbNested } from "../databases/mariadb";
|
||||
import type { MongoNested } from "../databases/mongo";
|
||||
import type { MysqlNested } from "../databases/mysql";
|
||||
@@ -258,6 +259,48 @@ export const cleanupSystem = async (serverId?: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
export interface DockerDiskUsageItem {
|
||||
type: string;
|
||||
totalCount: number;
|
||||
active: number;
|
||||
size: string;
|
||||
reclaimable: string;
|
||||
sizeBytes: number;
|
||||
}
|
||||
|
||||
const parseSizeToBytes = (size: string): number => {
|
||||
const match = size.match(/^([\d.]+)\s*([KMGT]?B)$/i);
|
||||
if (!match) return 0;
|
||||
const value = Number.parseFloat(match[1] as string);
|
||||
const unit = (match[2] as string).toUpperCase();
|
||||
const multipliers: Record<string, number> = {
|
||||
B: 1,
|
||||
KB: 1024,
|
||||
MB: 1024 ** 2,
|
||||
GB: 1024 ** 3,
|
||||
TB: 1024 ** 4,
|
||||
};
|
||||
return value * (multipliers[unit] || 0);
|
||||
};
|
||||
|
||||
export const getDockerDiskUsage = async (): Promise<DockerDiskUsageItem[]> => {
|
||||
const command = "docker system df --format '{{json .}}'";
|
||||
const { stdout } = await execAsync(command);
|
||||
|
||||
const lines = stdout.trim().split("\n").filter(Boolean);
|
||||
return lines.map((line) => {
|
||||
const data = JSON.parse(line);
|
||||
return {
|
||||
type: data.Type,
|
||||
totalCount: Number.parseInt(data.TotalCount, 10) || 0,
|
||||
active: Number.parseInt(data.Active, 10) || 0,
|
||||
size: data.Size,
|
||||
reclaimable: data.Reclaimable,
|
||||
sizeBytes: parseSizeToBytes(data.Size),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Volume cleanup should always be performed manually by the user. The reason is that during automatic cleanup, a volume may be deleted due to a stopped container, which is a dangerous situation.
|
||||
*
|
||||
@@ -433,7 +476,7 @@ export const parseEnvironmentKeyValuePair = (
|
||||
return [key, valueParts.join("=")];
|
||||
};
|
||||
|
||||
export const getEnviromentVariablesObject = (
|
||||
export const getEnvironmentVariablesObject = (
|
||||
input: string | null,
|
||||
projectEnv?: string | null,
|
||||
environmentEnv?: string | null,
|
||||
@@ -511,11 +554,6 @@ export const generateConfigContainer = (
|
||||
ulimitsSwarm,
|
||||
} = application;
|
||||
|
||||
const sanitizedStopGracePeriodSwarm =
|
||||
typeof stopGracePeriodSwarm === "bigint"
|
||||
? Number(stopGracePeriodSwarm)
|
||||
: stopGracePeriodSwarm;
|
||||
|
||||
const haveMounts = mounts && mounts.length > 0;
|
||||
|
||||
return {
|
||||
@@ -550,9 +588,15 @@ export const generateConfigContainer = (
|
||||
},
|
||||
},
|
||||
}),
|
||||
...(rollbackConfigSwarm && {
|
||||
RollbackConfig: rollbackConfigSwarm,
|
||||
}),
|
||||
...(rollbackConfigSwarm
|
||||
? { RollbackConfig: rollbackConfigSwarm }
|
||||
: {
|
||||
// default rollback config to match update config
|
||||
RollbackConfig: {
|
||||
Parallelism: 1,
|
||||
Order: "start-first",
|
||||
},
|
||||
}),
|
||||
...(updateConfigSwarm
|
||||
? { UpdateConfig: updateConfigSwarm }
|
||||
: {
|
||||
@@ -560,11 +604,12 @@ export const generateConfigContainer = (
|
||||
UpdateConfig: {
|
||||
Parallelism: 1,
|
||||
Order: "start-first",
|
||||
FailureAction: "rollback",
|
||||
},
|
||||
}),
|
||||
...(sanitizedStopGracePeriodSwarm !== null &&
|
||||
sanitizedStopGracePeriodSwarm !== undefined && {
|
||||
StopGracePeriod: sanitizedStopGracePeriodSwarm,
|
||||
...(stopGracePeriodSwarm !== null &&
|
||||
stopGracePeriodSwarm !== undefined && {
|
||||
StopGracePeriod: stopGracePeriodSwarm,
|
||||
}),
|
||||
...(networkSwarm
|
||||
? {
|
||||
@@ -610,6 +655,7 @@ export const generateFileMounts = (
|
||||
appName: string,
|
||||
service:
|
||||
| ApplicationNested
|
||||
| LibsqlNested
|
||||
| MongoNested
|
||||
| MariadbNested
|
||||
| MysqlNested
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
sendEmailNotification,
|
||||
sendGotifyNotification,
|
||||
sendLarkNotification,
|
||||
sendMattermostNotification,
|
||||
sendNtfyNotification,
|
||||
sendPushoverNotification,
|
||||
sendResendNotification,
|
||||
@@ -50,6 +51,7 @@ export const sendBuildErrorNotifications = async ({
|
||||
resend: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
mattermost: true,
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
@@ -66,6 +68,7 @@ export const sendBuildErrorNotifications = async ({
|
||||
slack,
|
||||
gotify,
|
||||
ntfy,
|
||||
mattermost,
|
||||
custom,
|
||||
lark,
|
||||
pushover,
|
||||
@@ -250,6 +253,26 @@ export const sendBuildErrorNotifications = async ({
|
||||
});
|
||||
}
|
||||
|
||||
if (mattermost) {
|
||||
await sendMattermostNotification(mattermost, {
|
||||
text: `:warning: **Build Failed**
|
||||
|
||||
**Project:** ${projectName}
|
||||
**Application:** ${applicationName}
|
||||
**Type:** ${applicationType}
|
||||
**Time:** ${date.toLocaleString()}
|
||||
|
||||
**Error:**
|
||||
\`\`\`
|
||||
${errorMessage}
|
||||
\`\`\`
|
||||
|
||||
[View Build Details](${buildLink})`,
|
||||
channel: mattermost.channel,
|
||||
username: mattermost.username || "Dokploy Bot",
|
||||
});
|
||||
}
|
||||
|
||||
if (custom) {
|
||||
await sendCustomNotification(custom, {
|
||||
title: "Build Error",
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
sendEmailNotification,
|
||||
sendGotifyNotification,
|
||||
sendLarkNotification,
|
||||
sendMattermostNotification,
|
||||
sendNtfyNotification,
|
||||
sendPushoverNotification,
|
||||
sendResendNotification,
|
||||
@@ -53,6 +54,7 @@ export const sendBuildSuccessNotifications = async ({
|
||||
resend: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
mattermost: true,
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
@@ -69,6 +71,7 @@ export const sendBuildSuccessNotifications = async ({
|
||||
slack,
|
||||
gotify,
|
||||
ntfy,
|
||||
mattermost,
|
||||
custom,
|
||||
lark,
|
||||
pushover,
|
||||
@@ -266,6 +269,14 @@ export const sendBuildSuccessNotifications = async ({
|
||||
});
|
||||
}
|
||||
|
||||
if (mattermost) {
|
||||
await sendMattermostNotification(mattermost, {
|
||||
text: `**✅ Build Success**\n\n**Project:** ${projectName}\n**Application:** ${applicationName}\n**Type:** ${applicationType}\n**Date:** ${format(date, "PP")}\n**Time:** ${format(date, "pp")}\n\n[View Build Details](${buildLink})`,
|
||||
channel: mattermost.channel,
|
||||
username: mattermost.username || "Dokploy",
|
||||
});
|
||||
}
|
||||
|
||||
if (custom) {
|
||||
await sendCustomNotification(custom, {
|
||||
title: "Build Success",
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
sendEmailNotification,
|
||||
sendGotifyNotification,
|
||||
sendLarkNotification,
|
||||
sendMattermostNotification,
|
||||
sendNtfyNotification,
|
||||
sendPushoverNotification,
|
||||
sendResendNotification,
|
||||
@@ -29,7 +30,7 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
}: {
|
||||
projectName: string;
|
||||
applicationName: string;
|
||||
databaseType: "postgres" | "mysql" | "mongodb" | "mariadb";
|
||||
databaseType: "postgres" | "mysql" | "mongodb" | "mariadb" | "libsql";
|
||||
type: "error" | "success";
|
||||
organizationId: string;
|
||||
errorMessage?: string;
|
||||
@@ -50,6 +51,7 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
resend: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
mattermost: true,
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
@@ -66,6 +68,7 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
slack,
|
||||
gotify,
|
||||
ntfy,
|
||||
mattermost,
|
||||
custom,
|
||||
lark,
|
||||
pushover,
|
||||
@@ -272,6 +275,21 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
});
|
||||
}
|
||||
|
||||
if (mattermost) {
|
||||
const statusEmoji = type === "success" ? "✅" : "❌";
|
||||
const typeStatus = type === "success" ? "Successful" : "Failed";
|
||||
const errorMsg =
|
||||
type === "error" && errorMessage
|
||||
? `\n\n**Error:**\n\`\`\`\n${errorMessage}\n\`\`\``
|
||||
: "";
|
||||
|
||||
await sendMattermostNotification(mattermost, {
|
||||
text: `**${statusEmoji} Database Backup ${typeStatus}**\n\n**Project:** ${projectName}\n**Application:** ${applicationName}\n**Type:** ${databaseType}\n**Database Name:** ${databaseName}\n**Date:** ${format(date, "PP")}\n**Time:** ${format(date, "pp")}${errorMsg}`,
|
||||
channel: mattermost.channel,
|
||||
username: mattermost.username || "Dokploy",
|
||||
});
|
||||
}
|
||||
|
||||
if (custom) {
|
||||
await sendCustomNotification(custom, {
|
||||
title: `Database Backup ${type === "success" ? "Successful" : "Failed"}`,
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
sendEmailNotification,
|
||||
sendGotifyNotification,
|
||||
sendLarkNotification,
|
||||
sendMattermostNotification,
|
||||
sendNtfyNotification,
|
||||
sendPushoverNotification,
|
||||
sendResendNotification,
|
||||
@@ -37,6 +38,7 @@ export const sendDockerCleanupNotifications = async (
|
||||
resend: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
mattermost: true,
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
@@ -53,6 +55,7 @@ export const sendDockerCleanupNotifications = async (
|
||||
slack,
|
||||
gotify,
|
||||
ntfy,
|
||||
mattermost,
|
||||
custom,
|
||||
lark,
|
||||
pushover,
|
||||
@@ -168,6 +171,14 @@ export const sendDockerCleanupNotifications = async (
|
||||
});
|
||||
}
|
||||
|
||||
if (mattermost) {
|
||||
await sendMattermostNotification(mattermost, {
|
||||
text: `**✅ Docker Cleanup**\n\n**Message:** ${message}\n**Date:** ${format(date, "PP")}\n**Time:** ${format(date, "pp")}`,
|
||||
channel: mattermost.channel,
|
||||
username: mattermost.username || "Dokploy",
|
||||
});
|
||||
}
|
||||
|
||||
if (custom) {
|
||||
await sendCustomNotification(custom, {
|
||||
title: "Docker Cleanup",
|
||||
|
||||
416
packages/server/src/utils/notifications/dokploy-backup.ts
Normal file
416
packages/server/src/utils/notifications/dokploy-backup.ts
Normal file
@@ -0,0 +1,416 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { notifications } from "@dokploy/server/db/schema";
|
||||
import DokployBackupEmail from "@dokploy/server/emails/emails/dokploy-backup";
|
||||
import { renderAsync } from "@react-email/components";
|
||||
import { format } from "date-fns";
|
||||
import { eq } from "drizzle-orm";
|
||||
import {
|
||||
sendCustomNotification,
|
||||
sendDiscordNotification,
|
||||
sendEmailNotification,
|
||||
sendGotifyNotification,
|
||||
sendLarkNotification,
|
||||
sendMattermostNotification,
|
||||
sendNtfyNotification,
|
||||
sendPushoverNotification,
|
||||
sendResendNotification,
|
||||
sendSlackNotification,
|
||||
sendTeamsNotification,
|
||||
sendTelegramNotification,
|
||||
} from "./utils";
|
||||
|
||||
export const sendDokployBackupNotifications = async ({
|
||||
type,
|
||||
errorMessage,
|
||||
backupSize,
|
||||
}: {
|
||||
type: "error" | "success";
|
||||
errorMessage?: string;
|
||||
backupSize?: string;
|
||||
}) => {
|
||||
const date = new Date();
|
||||
const unixDate = ~~(Number(date) / 1000);
|
||||
const notificationList = await db.query.notifications.findMany({
|
||||
where: eq(notifications.dokployBackup, true),
|
||||
with: {
|
||||
email: true,
|
||||
discord: true,
|
||||
telegram: true,
|
||||
slack: true,
|
||||
resend: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
mattermost: true,
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
teams: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const notification of notificationList) {
|
||||
const {
|
||||
email,
|
||||
discord,
|
||||
telegram,
|
||||
slack,
|
||||
resend,
|
||||
gotify,
|
||||
ntfy,
|
||||
mattermost,
|
||||
custom,
|
||||
lark,
|
||||
pushover,
|
||||
teams,
|
||||
} = notification;
|
||||
|
||||
try {
|
||||
if (email || resend) {
|
||||
const template = await renderAsync(
|
||||
DokployBackupEmail({
|
||||
type,
|
||||
errorMessage,
|
||||
date: date.toLocaleString(),
|
||||
backupSize,
|
||||
}),
|
||||
).catch();
|
||||
|
||||
if (email) {
|
||||
await sendEmailNotification(
|
||||
email,
|
||||
"Dokploy instance backup",
|
||||
template,
|
||||
);
|
||||
}
|
||||
|
||||
if (resend) {
|
||||
await sendResendNotification(
|
||||
resend,
|
||||
"Dokploy instance backup",
|
||||
template,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (discord) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${discord.decoration ? decoration : ""} ${text}`.trim();
|
||||
|
||||
await sendDiscordNotification(discord, {
|
||||
title:
|
||||
type === "success"
|
||||
? decorate(">", "`✅` Dokploy Backup Successful")
|
||||
: decorate(">", "`❌` Dokploy Backup Failed"),
|
||||
color: type === "success" ? 0x57f287 : 0xed4245,
|
||||
fields: [
|
||||
{
|
||||
name: decorate("`📦`", "Backup Type"),
|
||||
value: "Complete Dokploy Instance",
|
||||
inline: true,
|
||||
},
|
||||
...(backupSize
|
||||
? [
|
||||
{
|
||||
name: decorate("`💾`", "Backup Size"),
|
||||
value: backupSize,
|
||||
inline: true,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
name: decorate("`📅`", "Date"),
|
||||
value: `<t:${unixDate}:D>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`⌚`", "Time"),
|
||||
value: `<t:${unixDate}:t>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`❓`", "Status"),
|
||||
value: type
|
||||
.replace("error", "Failed")
|
||||
.replace("success", "Successful"),
|
||||
inline: true,
|
||||
},
|
||||
...(type === "error" && errorMessage
|
||||
? [
|
||||
{
|
||||
name: decorate("`⚠️`", "Error Message"),
|
||||
value: `\`\`\`${errorMessage}\`\`\``,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
timestamp: date.toISOString(),
|
||||
footer: {
|
||||
text: "Dokploy Instance Backup Notification",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (gotify) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${gotify.decoration ? decoration : ""} ${text}\n`;
|
||||
|
||||
await sendGotifyNotification(
|
||||
gotify,
|
||||
decorate(
|
||||
type === "success" ? "✅" : "❌",
|
||||
`Dokploy Backup ${type === "success" ? "Successful" : "Failed"}`,
|
||||
),
|
||||
`${decorate("📦", "Backup Type: Complete Dokploy Instance")}` +
|
||||
`${backupSize ? decorate("💾", `Backup Size: ${backupSize}`) : ""}` +
|
||||
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
|
||||
`${type === "error" && errorMessage ? decorate("❌", `Error:\n${errorMessage}`) : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (ntfy) {
|
||||
await sendNtfyNotification(
|
||||
ntfy,
|
||||
`Dokploy Backup ${type === "success" ? "Successful" : "Failed"}`,
|
||||
`${type === "success" ? "white_check_mark" : "x"}`,
|
||||
"",
|
||||
"📦Backup Type: Complete Dokploy Instance\n" +
|
||||
`${backupSize ? `💾Backup Size: ${backupSize}\n` : ""}` +
|
||||
`🕒Date: ${date.toLocaleString()}\n` +
|
||||
`${type === "error" && errorMessage ? `❌Error:\n${errorMessage}` : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (telegram) {
|
||||
const isError = type === "error" && errorMessage;
|
||||
|
||||
const statusEmoji = type === "success" ? "✅" : "❌";
|
||||
const typeStatus = type === "success" ? "Successful" : "Failed";
|
||||
const errorMsg = isError
|
||||
? `\n\n<b>Error:</b>\n<pre>${errorMessage}</pre>`
|
||||
: "";
|
||||
const sizeInfo = backupSize
|
||||
? `\n<b>Backup Size:</b> ${backupSize}`
|
||||
: "";
|
||||
|
||||
const messageText = `<b>${statusEmoji} Dokploy Backup ${typeStatus}</b>\n\n<b>Backup Type:</b> Complete Dokploy Instance${sizeInfo}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}${isError ? errorMsg : ""}`;
|
||||
|
||||
await sendTelegramNotification(telegram, messageText);
|
||||
}
|
||||
|
||||
if (slack) {
|
||||
const { channel } = slack;
|
||||
await sendSlackNotification(slack, {
|
||||
channel: channel,
|
||||
attachments: [
|
||||
{
|
||||
color: type === "success" ? "#00FF00" : "#FF0000",
|
||||
pretext:
|
||||
type === "success"
|
||||
? ":white_check_mark: *Dokploy Backup Successful*"
|
||||
: ":x: *Dokploy Backup Failed*",
|
||||
fields: [
|
||||
...(type === "error" && errorMessage
|
||||
? [
|
||||
{
|
||||
title: "Error Message",
|
||||
value: errorMessage,
|
||||
short: false,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: "Backup Type",
|
||||
value: "Complete Dokploy Instance",
|
||||
short: true,
|
||||
},
|
||||
...(backupSize
|
||||
? [
|
||||
{
|
||||
title: "Backup Size",
|
||||
value: backupSize,
|
||||
short: true,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: "Time",
|
||||
value: date.toLocaleString(),
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Status",
|
||||
value: type === "success" ? "Successful" : "Failed",
|
||||
short: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (lark) {
|
||||
const limitCharacter = 800;
|
||||
const truncatedErrorMessage =
|
||||
errorMessage && errorMessage.length > limitCharacter
|
||||
? errorMessage.substring(0, limitCharacter)
|
||||
: errorMessage;
|
||||
|
||||
await sendLarkNotification(lark, {
|
||||
msg_type: "interactive",
|
||||
card: {
|
||||
schema: "2.0",
|
||||
config: {
|
||||
update_multi: true,
|
||||
style: {
|
||||
text_size: {
|
||||
normal_v2: {
|
||||
default: "normal",
|
||||
pc: "normal",
|
||||
mobile: "heading",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
header: {
|
||||
title: {
|
||||
tag: "plain_text",
|
||||
content:
|
||||
type === "success"
|
||||
? "✅ Dokploy Backup Successful"
|
||||
: "❌ Dokploy Backup Failed",
|
||||
},
|
||||
subtitle: {
|
||||
tag: "plain_text",
|
||||
content: "",
|
||||
},
|
||||
template: type === "success" ? "green" : "red",
|
||||
padding: "12px 12px 12px 12px",
|
||||
},
|
||||
body: {
|
||||
direction: "vertical",
|
||||
padding: "12px 12px 12px 12px",
|
||||
elements: [
|
||||
{
|
||||
tag: "column_set",
|
||||
columns: [
|
||||
{
|
||||
tag: "column",
|
||||
width: "weighted",
|
||||
elements: [
|
||||
{
|
||||
tag: "markdown",
|
||||
content:
|
||||
"**Backup Type:**\nComplete Dokploy Instance",
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Status:**\n${type === "success" ? "Successful" : "Failed"}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
],
|
||||
vertical_align: "top",
|
||||
weight: 1,
|
||||
},
|
||||
{
|
||||
tag: "column",
|
||||
width: "weighted",
|
||||
elements: [
|
||||
...(backupSize
|
||||
? [
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Backup Size:**\n${backupSize}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Date:**\n${format(date, "PP pp")}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
],
|
||||
vertical_align: "top",
|
||||
weight: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
...(type === "error" && truncatedErrorMessage
|
||||
? [
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Error Message:**\n\`\`\`\n${truncatedErrorMessage}\n\`\`\``,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (mattermost) {
|
||||
const statusEmoji = type === "success" ? ":white_check_mark:" : ":x:";
|
||||
const typeStatus = type === "success" ? "Successful" : "Failed";
|
||||
await sendMattermostNotification(mattermost, {
|
||||
text: `${statusEmoji} **Dokploy Backup ${typeStatus}**
|
||||
|
||||
**Backup Type:** Complete Dokploy Instance${backupSize ? `\n**Backup Size:** ${backupSize}` : ""}
|
||||
**Date:** ${date.toLocaleString()}
|
||||
**Status:** ${typeStatus}${type === "error" && errorMessage ? `\n\n**Error:**\n\`\`\`\n${errorMessage}\n\`\`\`` : ""}`,
|
||||
channel: mattermost.channel,
|
||||
username: mattermost.username || "Dokploy Bot",
|
||||
});
|
||||
}
|
||||
|
||||
if (custom) {
|
||||
await sendCustomNotification(custom, {
|
||||
title: `Dokploy Backup ${type === "success" ? "Successful" : "Failed"}`,
|
||||
message: `Dokploy instance backup ${type === "success" ? "completed successfully" : "failed"}`,
|
||||
backupType: "Complete Dokploy Instance",
|
||||
...(backupSize ? { backupSize } : {}),
|
||||
...(type === "error" && errorMessage ? { errorMessage } : {}),
|
||||
timestamp: date.toISOString(),
|
||||
date: date.toLocaleString(),
|
||||
status: type,
|
||||
type: "dokploy-backup",
|
||||
});
|
||||
}
|
||||
|
||||
if (pushover) {
|
||||
await sendPushoverNotification(
|
||||
pushover,
|
||||
`Dokploy Backup ${type === "success" ? "Successful" : "Failed"}`,
|
||||
`Backup Type: Complete Dokploy Instance${backupSize ? `\nBackup Size: ${backupSize}` : ""}\nDate: ${date.toLocaleString()}${type === "error" && errorMessage ? `\nError: ${errorMessage}` : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (teams) {
|
||||
await sendTeamsNotification(teams, {
|
||||
title: `${type === "success" ? "✅" : "❌"} Dokploy Backup ${type === "success" ? "Successful" : "Failed"}`,
|
||||
facts: [
|
||||
{ name: "Backup Type", value: "Complete Dokploy Instance" },
|
||||
...(backupSize ? [{ name: "Backup Size", value: backupSize }] : []),
|
||||
{ name: "Date", value: format(date, "PP pp") },
|
||||
{
|
||||
name: "Status",
|
||||
value: type === "success" ? "Successful" : "Failed",
|
||||
},
|
||||
...(type === "error" && errorMessage
|
||||
? [{ name: "Error Message", value: errorMessage }]
|
||||
: []),
|
||||
],
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
sendEmailNotification,
|
||||
sendGotifyNotification,
|
||||
sendLarkNotification,
|
||||
sendMattermostNotification,
|
||||
sendNtfyNotification,
|
||||
sendPushoverNotification,
|
||||
sendResendNotification,
|
||||
@@ -32,6 +33,7 @@ export const sendDokployRestartNotifications = async () => {
|
||||
resend: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
mattermost: true,
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
@@ -48,6 +50,7 @@ export const sendDokployRestartNotifications = async () => {
|
||||
slack,
|
||||
gotify,
|
||||
ntfy,
|
||||
mattermost,
|
||||
custom,
|
||||
lark,
|
||||
pushover,
|
||||
@@ -158,6 +161,14 @@ export const sendDokployRestartNotifications = async () => {
|
||||
});
|
||||
}
|
||||
|
||||
if (mattermost) {
|
||||
await sendMattermostNotification(mattermost, {
|
||||
text: `**✅ Dokploy Server Restarted**\n\n**Date:** ${format(date, "PP")}\n**Time:** ${format(date, "pp")}`,
|
||||
channel: mattermost.channel,
|
||||
username: mattermost.username || "Dokploy",
|
||||
});
|
||||
}
|
||||
|
||||
if (custom) {
|
||||
try {
|
||||
await sendCustomNotification(custom, {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
sendCustomNotification,
|
||||
sendDiscordNotification,
|
||||
sendLarkNotification,
|
||||
sendMattermostNotification,
|
||||
sendPushoverNotification,
|
||||
sendSlackNotification,
|
||||
sendTeamsNotification,
|
||||
@@ -38,6 +39,7 @@ export const sendServerThresholdNotifications = async (
|
||||
discord: true,
|
||||
telegram: true,
|
||||
slack: true,
|
||||
mattermost: true,
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
@@ -49,63 +51,72 @@ export const sendServerThresholdNotifications = async (
|
||||
const typeColor = 0xff0000; // Rojo para indicar alerta
|
||||
|
||||
for (const notification of notificationList) {
|
||||
const { discord, telegram, slack, custom, lark, pushover, teams } =
|
||||
notification;
|
||||
const {
|
||||
discord,
|
||||
telegram,
|
||||
slack,
|
||||
mattermost,
|
||||
custom,
|
||||
lark,
|
||||
pushover,
|
||||
teams,
|
||||
} = notification;
|
||||
|
||||
if (discord) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${discord.decoration ? decoration : ""} ${text}`.trim();
|
||||
try {
|
||||
if (discord) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${discord.decoration ? decoration : ""} ${text}`.trim();
|
||||
|
||||
await sendDiscordNotification(discord, {
|
||||
title: decorate(">", `\`⚠️\` Server ${payload.Type} Alert`),
|
||||
color: typeColor,
|
||||
fields: [
|
||||
{
|
||||
name: decorate("`🏷️`", "Server Name"),
|
||||
value: payload.ServerName,
|
||||
inline: true,
|
||||
await sendDiscordNotification(discord, {
|
||||
title: decorate(">", `\`⚠️\` Server ${payload.Type} Alert`),
|
||||
color: typeColor,
|
||||
fields: [
|
||||
{
|
||||
name: decorate("`🏷️`", "Server Name"),
|
||||
value: payload.ServerName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`📅`", "Date"),
|
||||
value: `<t:${unixDate}:D>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`⌚`", "Time"),
|
||||
value: `<t:${unixDate}:t>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate(typeEmoji, "Type"),
|
||||
value: payload.Type,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("📊", "Current Value"),
|
||||
value: `${payload.Value.toFixed(2)}%`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("⚠️", "Threshold"),
|
||||
value: `${payload.Threshold.toFixed(2)}%`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`📜`", "Message"),
|
||||
value: `\`\`\`${payload.Message}\`\`\``,
|
||||
},
|
||||
],
|
||||
timestamp: date.toISOString(),
|
||||
footer: {
|
||||
text: "Dokploy Server Monitoring Alert",
|
||||
},
|
||||
{
|
||||
name: decorate("`📅`", "Date"),
|
||||
value: `<t:${unixDate}:D>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`⌚`", "Time"),
|
||||
value: `<t:${unixDate}:t>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate(typeEmoji, "Type"),
|
||||
value: payload.Type,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("📊", "Current Value"),
|
||||
value: `${payload.Value.toFixed(2)}%`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("⚠️", "Threshold"),
|
||||
value: `${payload.Threshold.toFixed(2)}%`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`📜`", "Message"),
|
||||
value: `\`\`\`${payload.Message}\`\`\``,
|
||||
},
|
||||
],
|
||||
timestamp: date.toISOString(),
|
||||
footer: {
|
||||
text: "Dokploy Server Monitoring Alert",
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (telegram) {
|
||||
await sendTelegramNotification(
|
||||
telegram,
|
||||
`
|
||||
if (telegram) {
|
||||
await sendTelegramNotification(
|
||||
telegram,
|
||||
`
|
||||
<b>⚠️ Server ${payload.Type} Alert</b>
|
||||
<b>Server Name:</b> ${payload.ServerName}
|
||||
<b>Type:</b> ${payload.Type}
|
||||
@@ -114,170 +125,181 @@ export const sendServerThresholdNotifications = async (
|
||||
<b>Message:</b> ${payload.Message}
|
||||
<b>Time:</b> ${date.toLocaleString()}
|
||||
`,
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (slack) {
|
||||
const { channel } = slack;
|
||||
await sendSlackNotification(slack, {
|
||||
channel: channel,
|
||||
attachments: [
|
||||
{
|
||||
color: "#FF0000",
|
||||
pretext: `:warning: *Server ${payload.Type} Alert*`,
|
||||
fields: [
|
||||
{
|
||||
title: "Server Name",
|
||||
value: payload.ServerName,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Type",
|
||||
value: payload.Type,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Current Value",
|
||||
value: `${payload.Value.toFixed(2)}%`,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Threshold",
|
||||
value: `${payload.Threshold.toFixed(2)}%`,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Message",
|
||||
value: payload.Message,
|
||||
},
|
||||
{
|
||||
title: "Time",
|
||||
value: date.toLocaleString(),
|
||||
short: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (slack) {
|
||||
const { channel } = slack;
|
||||
await sendSlackNotification(slack, {
|
||||
channel: channel,
|
||||
attachments: [
|
||||
{
|
||||
color: "#FF0000",
|
||||
pretext: `:warning: *Server ${payload.Type} Alert*`,
|
||||
fields: [
|
||||
{
|
||||
title: "Server Name",
|
||||
value: payload.ServerName,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Type",
|
||||
value: payload.Type,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Current Value",
|
||||
value: `${payload.Value.toFixed(2)}%`,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Threshold",
|
||||
value: `${payload.Threshold.toFixed(2)}%`,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Message",
|
||||
value: payload.Message,
|
||||
},
|
||||
{
|
||||
title: "Time",
|
||||
value: date.toLocaleString(),
|
||||
short: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (custom) {
|
||||
await sendCustomNotification(custom, {
|
||||
title: `Server ${payload.Type} Alert`,
|
||||
message: payload.Message,
|
||||
serverName: payload.ServerName,
|
||||
type: payload.Type,
|
||||
currentValue: payload.Value,
|
||||
threshold: payload.Threshold,
|
||||
timestamp: date.toISOString(),
|
||||
date: date.toLocaleString(),
|
||||
status: "alert",
|
||||
alertType: "server-threshold",
|
||||
});
|
||||
}
|
||||
if (mattermost) {
|
||||
await sendMattermostNotification(mattermost, {
|
||||
text: `**⚠️ Server ${payload.Type} Alert**\n\n**Server Name:** ${payload.ServerName}\n**Type:** ${payload.Type}\n**Current Value:** ${payload.Value.toFixed(2)}%\n**Threshold:** ${payload.Threshold.toFixed(2)}%\n**Message:** ${payload.Message}\n**Time:** ${date.toLocaleString()}`,
|
||||
channel: mattermost.channel,
|
||||
username: mattermost.username || "Dokploy",
|
||||
});
|
||||
}
|
||||
|
||||
if (lark) {
|
||||
await sendLarkNotification(lark, {
|
||||
msg_type: "interactive",
|
||||
card: {
|
||||
schema: "2.0",
|
||||
config: {
|
||||
update_multi: true,
|
||||
style: {
|
||||
text_size: {
|
||||
normal_v2: {
|
||||
default: "normal",
|
||||
pc: "normal",
|
||||
mobile: "heading",
|
||||
if (custom) {
|
||||
await sendCustomNotification(custom, {
|
||||
title: `Server ${payload.Type} Alert`,
|
||||
message: payload.Message,
|
||||
serverName: payload.ServerName,
|
||||
type: payload.Type,
|
||||
currentValue: payload.Value,
|
||||
threshold: payload.Threshold,
|
||||
timestamp: date.toISOString(),
|
||||
date: date.toLocaleString(),
|
||||
status: "alert",
|
||||
alertType: "server-threshold",
|
||||
});
|
||||
}
|
||||
|
||||
if (lark) {
|
||||
await sendLarkNotification(lark, {
|
||||
msg_type: "interactive",
|
||||
card: {
|
||||
schema: "2.0",
|
||||
config: {
|
||||
update_multi: true,
|
||||
style: {
|
||||
text_size: {
|
||||
normal_v2: {
|
||||
default: "normal",
|
||||
pc: "normal",
|
||||
mobile: "heading",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
header: {
|
||||
title: {
|
||||
tag: "plain_text",
|
||||
content: `⚠️ Server ${payload.Type} Alert`,
|
||||
},
|
||||
subtitle: {
|
||||
tag: "plain_text",
|
||||
content: "",
|
||||
},
|
||||
template: "red",
|
||||
padding: "12px 12px 12px 12px",
|
||||
},
|
||||
body: {
|
||||
direction: "vertical",
|
||||
padding: "12px 12px 12px 12px",
|
||||
elements: [
|
||||
{
|
||||
tag: "column_set",
|
||||
columns: [
|
||||
{
|
||||
tag: "column",
|
||||
width: "weighted",
|
||||
elements: [
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Server Name:**\n${payload.ServerName}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Current Value:**\n${payload.Value.toFixed(2)}%`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Alert Message:**\n${payload.Message}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
],
|
||||
vertical_align: "top",
|
||||
weight: 1,
|
||||
},
|
||||
{
|
||||
tag: "column",
|
||||
width: "weighted",
|
||||
elements: [
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Type:**\n${payload.Type === "CPU" ? "🔲" : "💾"} ${payload.Type}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Threshold:**\n${payload.Threshold.toFixed(2)}%`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Alert Time:**\n${date.toLocaleString()}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
],
|
||||
vertical_align: "top",
|
||||
weight: 1,
|
||||
},
|
||||
],
|
||||
header: {
|
||||
title: {
|
||||
tag: "plain_text",
|
||||
content: `⚠️ Server ${payload.Type} Alert`,
|
||||
},
|
||||
],
|
||||
subtitle: {
|
||||
tag: "plain_text",
|
||||
content: "",
|
||||
},
|
||||
template: "red",
|
||||
padding: "12px 12px 12px 12px",
|
||||
},
|
||||
body: {
|
||||
direction: "vertical",
|
||||
padding: "12px 12px 12px 12px",
|
||||
elements: [
|
||||
{
|
||||
tag: "column_set",
|
||||
columns: [
|
||||
{
|
||||
tag: "column",
|
||||
width: "weighted",
|
||||
elements: [
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Server Name:**\n${payload.ServerName}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Current Value:**\n${payload.Value.toFixed(2)}%`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Alert Message:**\n${payload.Message}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
],
|
||||
vertical_align: "top",
|
||||
weight: 1,
|
||||
},
|
||||
{
|
||||
tag: "column",
|
||||
width: "weighted",
|
||||
elements: [
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Type:**\n${payload.Type === "CPU" ? "🔲" : "💾"} ${payload.Type}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Threshold:**\n${payload.Threshold.toFixed(2)}%`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Alert Time:**\n${date.toLocaleString()}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
],
|
||||
vertical_align: "top",
|
||||
weight: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (pushover) {
|
||||
await sendPushoverNotification(
|
||||
pushover,
|
||||
`Server ${payload.Type} Alert`,
|
||||
`Server: ${payload.ServerName}\nType: ${payload.Type}\nCurrent: ${payload.Value.toFixed(2)}%\nThreshold: ${payload.Threshold.toFixed(2)}%\nMessage: ${payload.Message}\nTime: ${date.toLocaleString()}`,
|
||||
);
|
||||
if (pushover) {
|
||||
await sendPushoverNotification(
|
||||
pushover,
|
||||
`Server ${payload.Type} Alert`,
|
||||
`Server: ${payload.ServerName}\nType: ${payload.Type}\nCurrent: ${payload.Value.toFixed(2)}%\nThreshold: ${payload.Threshold.toFixed(2)}%\nMessage: ${payload.Message}\nTime: ${date.toLocaleString()}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
if (teams) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
email,
|
||||
gotify,
|
||||
lark,
|
||||
mattermost,
|
||||
ntfy,
|
||||
pushover,
|
||||
resend,
|
||||
@@ -206,6 +207,33 @@ export const sendNtfyNotification = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const sendMattermostNotification = async (
|
||||
connection: typeof mattermost.$inferInsert,
|
||||
message: any,
|
||||
) => {
|
||||
const payload = {
|
||||
...message,
|
||||
// Only include username if it's provided and not empty
|
||||
...(message.username?.trim() && { username: message.username }),
|
||||
// Only include channel if it's provided and not empty
|
||||
...(message.channel?.trim() && {
|
||||
channel: `#${message.channel.replace("#", "")}`,
|
||||
}),
|
||||
};
|
||||
|
||||
const response = await fetch(connection.webhookUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to send Mattermost notification: ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const sendCustomNotification = async (
|
||||
connection: typeof custom.$inferInsert,
|
||||
payload: Record<string, any>,
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
sendDiscordNotification,
|
||||
sendEmailNotification,
|
||||
sendGotifyNotification,
|
||||
sendLarkNotification,
|
||||
sendMattermostNotification,
|
||||
sendNtfyNotification,
|
||||
sendPushoverNotification,
|
||||
sendResendNotification,
|
||||
@@ -37,7 +39,8 @@ export const sendVolumeBackupNotifications = async ({
|
||||
| "mongodb"
|
||||
| "mariadb"
|
||||
| "redis"
|
||||
| "compose";
|
||||
| "compose"
|
||||
| "libsql";
|
||||
type: "error" | "success";
|
||||
organizationId: string;
|
||||
errorMessage?: string;
|
||||
@@ -58,9 +61,11 @@ export const sendVolumeBackupNotifications = async ({
|
||||
resend: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
mattermost: true,
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
teams: true,
|
||||
custom: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -73,278 +78,420 @@ export const sendVolumeBackupNotifications = async ({
|
||||
slack,
|
||||
gotify,
|
||||
ntfy,
|
||||
mattermost,
|
||||
custom,
|
||||
lark,
|
||||
pushover,
|
||||
teams,
|
||||
custom,
|
||||
} = notification;
|
||||
|
||||
if (email || resend) {
|
||||
const subject = `Volume Backup ${type === "success" ? "Successful" : "Failed"} - ${applicationName}`;
|
||||
const htmlContent = await renderAsync(
|
||||
VolumeBackupEmail({
|
||||
try {
|
||||
if (email || resend) {
|
||||
const subject = `Volume Backup ${type === "success" ? "Successful" : "Failed"} - ${applicationName}`;
|
||||
const htmlContent = await renderAsync(
|
||||
VolumeBackupEmail({
|
||||
projectName,
|
||||
applicationName,
|
||||
volumeName,
|
||||
serviceType,
|
||||
type,
|
||||
errorMessage,
|
||||
backupSize,
|
||||
date: date.toISOString(),
|
||||
}),
|
||||
);
|
||||
if (email) {
|
||||
await sendEmailNotification(email, subject, htmlContent);
|
||||
}
|
||||
if (resend) {
|
||||
await sendResendNotification(resend, subject, htmlContent);
|
||||
}
|
||||
}
|
||||
|
||||
if (discord) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${discord.decoration ? decoration : ""} ${text}`.trim();
|
||||
|
||||
await sendDiscordNotification(discord, {
|
||||
title:
|
||||
type === "success"
|
||||
? decorate(">", "`✅` Volume Backup Successful")
|
||||
: decorate(">", "`❌` Volume Backup Failed"),
|
||||
color: type === "success" ? 0x57f287 : 0xed4245,
|
||||
fields: [
|
||||
{
|
||||
name: decorate("`🛠️`", "Project"),
|
||||
value: projectName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`⚙️`", "Application"),
|
||||
value: applicationName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`💾`", "Volume Name"),
|
||||
value: volumeName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`🔧`", "Service Type"),
|
||||
value: serviceType,
|
||||
inline: true,
|
||||
},
|
||||
...(backupSize
|
||||
? [
|
||||
{
|
||||
name: decorate("`📊`", "Backup Size"),
|
||||
value: backupSize,
|
||||
inline: true,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
name: decorate("`📅`", "Date"),
|
||||
value: `<t:${unixDate}:D>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`⌚`", "Time"),
|
||||
value: `<t:${unixDate}:t>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`❓`", "Type"),
|
||||
value: type
|
||||
.replace("error", "Failed")
|
||||
.replace("success", "Successful"),
|
||||
inline: true,
|
||||
},
|
||||
...(type === "error" && errorMessage
|
||||
? [
|
||||
{
|
||||
name: decorate("`⚠️`", "Error Message"),
|
||||
value: `\`\`\`${errorMessage}\`\`\``,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
timestamp: date.toISOString(),
|
||||
footer: {
|
||||
text: "Dokploy Volume Backup Notification",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (gotify) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${gotify.decoration ? decoration : ""} ${text}\n`;
|
||||
|
||||
await sendGotifyNotification(
|
||||
gotify,
|
||||
decorate(
|
||||
type === "success" ? "✅" : "❌",
|
||||
`Volume Backup ${type === "success" ? "Successful" : "Failed"}`,
|
||||
),
|
||||
`${decorate("🛠️", `Project: ${projectName}`)}` +
|
||||
`${decorate("⚙️", `Application: ${applicationName}`)}` +
|
||||
`${decorate("💾", `Volume Name: ${volumeName}`)}` +
|
||||
`${decorate("🔧", `Service Type: ${serviceType}`)}` +
|
||||
`${backupSize ? decorate("📊", `Backup Size: ${backupSize}`) : ""}` +
|
||||
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
|
||||
`${type === "error" && errorMessage ? decorate("❌", `Error:\n${errorMessage}`) : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (ntfy) {
|
||||
await sendNtfyNotification(
|
||||
ntfy,
|
||||
`Volume Backup ${type === "success" ? "Successful" : "Failed"}`,
|
||||
`${type === "success" ? "white_check_mark" : "x"}`,
|
||||
"",
|
||||
`🛠️Project: ${projectName}\n` +
|
||||
`⚙️Application: ${applicationName}\n` +
|
||||
`💾Volume Name: ${volumeName}\n` +
|
||||
`🔧Service Type: ${serviceType}\n` +
|
||||
`${backupSize ? `📊Backup Size: ${backupSize}\n` : ""}` +
|
||||
`🕒Date: ${date.toLocaleString()}\n` +
|
||||
`${type === "error" && errorMessage ? `❌Error:\n${errorMessage}` : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (telegram) {
|
||||
const isError = type === "error" && errorMessage;
|
||||
|
||||
const statusEmoji = type === "success" ? "✅" : "❌";
|
||||
const typeStatus = type === "success" ? "Successful" : "Failed";
|
||||
const errorMsg = isError
|
||||
? `\n\n<b>Error:</b>\n<pre>${errorMessage}</pre>`
|
||||
: "";
|
||||
const sizeInfo = backupSize
|
||||
? `\n<b>Backup Size:</b> ${backupSize}`
|
||||
: "";
|
||||
|
||||
const messageText = `<b>${statusEmoji} Volume Backup ${typeStatus}</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Volume Name:</b> ${volumeName}\n<b>Service Type:</b> ${serviceType}${sizeInfo}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}${isError ? errorMsg : ""}`;
|
||||
|
||||
await sendTelegramNotification(telegram, messageText);
|
||||
}
|
||||
|
||||
if (slack) {
|
||||
const { channel } = slack;
|
||||
await sendSlackNotification(slack, {
|
||||
channel: channel,
|
||||
attachments: [
|
||||
{
|
||||
color: type === "success" ? "#00FF00" : "#FF0000",
|
||||
pretext:
|
||||
type === "success"
|
||||
? ":white_check_mark: *Volume Backup Successful*"
|
||||
: ":x: *Volume Backup Failed*",
|
||||
fields: [
|
||||
...(type === "error" && errorMessage
|
||||
? [
|
||||
{
|
||||
title: "Error Message",
|
||||
value: errorMessage,
|
||||
short: false,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: "Project",
|
||||
value: projectName,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Application",
|
||||
value: applicationName,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Volume Name",
|
||||
value: volumeName,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Service Type",
|
||||
value: serviceType,
|
||||
short: true,
|
||||
},
|
||||
...(backupSize
|
||||
? [
|
||||
{
|
||||
title: "Backup Size",
|
||||
value: backupSize,
|
||||
short: true,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: "Time",
|
||||
value: date.toLocaleString(),
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Type",
|
||||
value: type,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Status",
|
||||
value: type === "success" ? "Successful" : "Failed",
|
||||
short: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (mattermost) {
|
||||
const statusEmoji = type === "success" ? "✅" : "❌";
|
||||
const typeStatus = type === "success" ? "Successful" : "Failed";
|
||||
const errorMsg =
|
||||
type === "error" && errorMessage
|
||||
? `\n\n**Error:**\n\`\`\`\n${errorMessage}\n\`\`\``
|
||||
: "";
|
||||
const sizeInfo = backupSize ? `\n**Backup Size:** ${backupSize}` : "";
|
||||
|
||||
await sendMattermostNotification(mattermost, {
|
||||
text: `**${statusEmoji} Volume Backup ${typeStatus}**\n\n**Project:** ${projectName}\n**Application:** ${applicationName}\n**Volume Name:** ${volumeName}\n**Service Type:** ${serviceType}${sizeInfo}\n**Date:** ${format(date, "PP")}\n**Time:** ${format(date, "pp")}${errorMsg}`,
|
||||
channel: mattermost.channel,
|
||||
username: mattermost.username || "Dokploy",
|
||||
});
|
||||
}
|
||||
|
||||
if (lark) {
|
||||
const limitCharacter = 800;
|
||||
const truncatedErrorMessage =
|
||||
errorMessage && errorMessage.length > limitCharacter
|
||||
? errorMessage.substring(0, limitCharacter)
|
||||
: errorMessage;
|
||||
|
||||
await sendLarkNotification(lark, {
|
||||
msg_type: "interactive",
|
||||
card: {
|
||||
schema: "2.0",
|
||||
config: {
|
||||
update_multi: true,
|
||||
style: {
|
||||
text_size: {
|
||||
normal_v2: {
|
||||
default: "normal",
|
||||
pc: "normal",
|
||||
mobile: "heading",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
header: {
|
||||
title: {
|
||||
tag: "plain_text",
|
||||
content:
|
||||
type === "success"
|
||||
? "✅ Volume Backup Successful"
|
||||
: "❌ Volume Backup Failed",
|
||||
},
|
||||
subtitle: {
|
||||
tag: "plain_text",
|
||||
content: "",
|
||||
},
|
||||
template: type === "success" ? "green" : "red",
|
||||
padding: "12px 12px 12px 12px",
|
||||
},
|
||||
body: {
|
||||
direction: "vertical",
|
||||
padding: "12px 12px 12px 12px",
|
||||
elements: [
|
||||
{
|
||||
tag: "column_set",
|
||||
columns: [
|
||||
{
|
||||
tag: "column",
|
||||
width: "weighted",
|
||||
elements: [
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Project:**\n${projectName}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Volume Name:**\n${volumeName}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Status:**\n${type === "success" ? "Successful" : "Failed"}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
],
|
||||
vertical_align: "top",
|
||||
weight: 1,
|
||||
},
|
||||
{
|
||||
tag: "column",
|
||||
width: "weighted",
|
||||
elements: [
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Application:**\n${applicationName}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Service Type:**\n${serviceType}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Date:**\n${format(date, "PP pp")}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
],
|
||||
vertical_align: "top",
|
||||
weight: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
...(type === "error" && truncatedErrorMessage
|
||||
? [
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Error Message:**\n\`\`\`\n${truncatedErrorMessage}\n\`\`\``,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (pushover) {
|
||||
await sendPushoverNotification(
|
||||
pushover,
|
||||
`Volume Backup ${type === "success" ? "Successful" : "Failed"}`,
|
||||
`Project: ${projectName}\nApplication: ${applicationName}\nVolume: ${volumeName}\nService Type: ${serviceType}${backupSize ? `\nBackup Size: ${backupSize}` : ""}\nDate: ${date.toLocaleString()}${type === "error" && errorMessage ? `\nError: ${errorMessage}` : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (teams) {
|
||||
const facts = [
|
||||
{ name: "Project", value: projectName },
|
||||
{ name: "Application", value: applicationName },
|
||||
{ name: "Volume Name", value: volumeName },
|
||||
{ name: "Service Type", value: serviceType },
|
||||
{ name: "Date", value: format(date, "PP pp") },
|
||||
{
|
||||
name: "Status",
|
||||
value: type === "success" ? "Successful" : "Failed",
|
||||
},
|
||||
];
|
||||
if (backupSize) {
|
||||
facts.push({ name: "Backup Size", value: backupSize });
|
||||
}
|
||||
if (type === "error" && errorMessage) {
|
||||
facts.push({ name: "Error", value: errorMessage.substring(0, 500) });
|
||||
}
|
||||
await sendTeamsNotification(teams, {
|
||||
title:
|
||||
type === "success"
|
||||
? "✅ Volume Backup Successful"
|
||||
: "❌ Volume Backup Failed",
|
||||
facts,
|
||||
});
|
||||
}
|
||||
|
||||
if (custom) {
|
||||
await sendCustomNotification(custom, {
|
||||
title: `Volume Backup ${type === "success" ? "Successful" : "Failed"}`,
|
||||
message:
|
||||
type === "success"
|
||||
? "Volume backup completed successfully"
|
||||
: "Volume backup failed",
|
||||
projectName,
|
||||
applicationName,
|
||||
volumeName,
|
||||
serviceType,
|
||||
type,
|
||||
errorMessage,
|
||||
backupSize,
|
||||
date: date.toISOString(),
|
||||
}),
|
||||
);
|
||||
if (email) {
|
||||
await sendEmailNotification(email, subject, htmlContent);
|
||||
errorMessage: errorMessage ?? "",
|
||||
backupSize: backupSize ?? "",
|
||||
timestamp: date.toISOString(),
|
||||
date: date.toLocaleString(),
|
||||
status: type,
|
||||
});
|
||||
}
|
||||
if (resend) {
|
||||
await sendResendNotification(resend, subject, htmlContent);
|
||||
}
|
||||
}
|
||||
|
||||
if (discord) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${discord.decoration ? decoration : ""} ${text}`.trim();
|
||||
|
||||
await sendDiscordNotification(discord, {
|
||||
title:
|
||||
type === "success"
|
||||
? decorate(">", "`✅` Volume Backup Successful")
|
||||
: decorate(">", "`❌` Volume Backup Failed"),
|
||||
color: type === "success" ? 0x57f287 : 0xed4245,
|
||||
fields: [
|
||||
{
|
||||
name: decorate("`🛠️`", "Project"),
|
||||
value: projectName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`⚙️`", "Application"),
|
||||
value: applicationName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`💾`", "Volume Name"),
|
||||
value: volumeName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`🔧`", "Service Type"),
|
||||
value: serviceType,
|
||||
inline: true,
|
||||
},
|
||||
...(backupSize
|
||||
? [
|
||||
{
|
||||
name: decorate("`📊`", "Backup Size"),
|
||||
value: backupSize,
|
||||
inline: true,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
name: decorate("`📅`", "Date"),
|
||||
value: `<t:${unixDate}:D>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`⌚`", "Time"),
|
||||
value: `<t:${unixDate}:t>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`❓`", "Type"),
|
||||
value: type
|
||||
.replace("error", "Failed")
|
||||
.replace("success", "Successful"),
|
||||
inline: true,
|
||||
},
|
||||
...(type === "error" && errorMessage
|
||||
? [
|
||||
{
|
||||
name: decorate("`⚠️`", "Error Message"),
|
||||
value: `\`\`\`${errorMessage.substring(0, 1010)}\`\`\``,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
timestamp: date.toISOString(),
|
||||
footer: {
|
||||
text: "Dokploy Volume Backup Notification",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (gotify) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${gotify.decoration ? decoration : ""} ${text}\n`;
|
||||
|
||||
await sendGotifyNotification(
|
||||
gotify,
|
||||
decorate(
|
||||
type === "success" ? "✅" : "❌",
|
||||
`Volume Backup ${type === "success" ? "Successful" : "Failed"}`,
|
||||
),
|
||||
`${decorate("🛠️", `Project: ${projectName}`)}` +
|
||||
`${decorate("⚙️", `Application: ${applicationName}`)}` +
|
||||
`${decorate("💾", `Volume Name: ${volumeName}`)}` +
|
||||
`${decorate("🔧", `Service Type: ${serviceType}`)}` +
|
||||
`${backupSize ? decorate("📊", `Backup Size: ${backupSize}`) : ""}` +
|
||||
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
|
||||
`${type === "error" && errorMessage ? decorate("❌", `Error:\n${errorMessage}`) : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (ntfy) {
|
||||
await sendNtfyNotification(
|
||||
ntfy,
|
||||
`Volume Backup ${type === "success" ? "Successful" : "Failed"}`,
|
||||
`${type === "success" ? "white_check_mark" : "x"}`,
|
||||
"",
|
||||
`🛠️Project: ${projectName}\n` +
|
||||
`⚙️Application: ${applicationName}\n` +
|
||||
`💾Volume Name: ${volumeName}\n` +
|
||||
`🔧Service Type: ${serviceType}\n` +
|
||||
`${backupSize ? `📊Backup Size: ${backupSize}\n` : ""}` +
|
||||
`🕒Date: ${date.toLocaleString()}\n` +
|
||||
`${type === "error" && errorMessage ? `❌Error:\n${errorMessage}` : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (telegram) {
|
||||
const isError = type === "error" && errorMessage;
|
||||
|
||||
const statusEmoji = type === "success" ? "✅" : "❌";
|
||||
const typeStatus = type === "success" ? "Successful" : "Failed";
|
||||
const errorMsg = isError
|
||||
? `\n\n<b>Error:</b>\n<pre>${errorMessage}</pre>`
|
||||
: "";
|
||||
const sizeInfo = backupSize ? `\n<b>Backup Size:</b> ${backupSize}` : "";
|
||||
|
||||
const messageText = `<b>${statusEmoji} Volume Backup ${typeStatus}</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Volume Name:</b> ${volumeName}\n<b>Service Type:</b> ${serviceType}${sizeInfo}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}${isError ? errorMsg : ""}`;
|
||||
|
||||
await sendTelegramNotification(telegram, messageText);
|
||||
}
|
||||
|
||||
if (slack) {
|
||||
const { channel } = slack;
|
||||
await sendSlackNotification(slack, {
|
||||
channel: channel,
|
||||
attachments: [
|
||||
{
|
||||
color: type === "success" ? "#00FF00" : "#FF0000",
|
||||
pretext:
|
||||
type === "success"
|
||||
? ":white_check_mark: *Volume Backup Successful*"
|
||||
: ":x: *Volume Backup Failed*",
|
||||
fields: [
|
||||
...(type === "error" && errorMessage
|
||||
? [
|
||||
{
|
||||
title: "Error Message",
|
||||
value: errorMessage,
|
||||
short: false,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: "Project",
|
||||
value: projectName,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Application",
|
||||
value: applicationName,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Volume Name",
|
||||
value: volumeName,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Service Type",
|
||||
value: serviceType,
|
||||
short: true,
|
||||
},
|
||||
...(backupSize
|
||||
? [
|
||||
{
|
||||
title: "Backup Size",
|
||||
value: backupSize,
|
||||
short: true,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: "Time",
|
||||
value: date.toLocaleString(),
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Type",
|
||||
value: type,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Status",
|
||||
value: type === "success" ? "Successful" : "Failed",
|
||||
short: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (pushover) {
|
||||
await sendPushoverNotification(
|
||||
pushover,
|
||||
`Volume Backup ${type === "success" ? "Successful" : "Failed"}`,
|
||||
`Project: ${projectName}\nApplication: ${applicationName}\nVolume: ${volumeName}\nService Type: ${serviceType}${backupSize ? `\nBackup Size: ${backupSize}` : ""}\nDate: ${date.toLocaleString()}${type === "error" && errorMessage ? `\nError: ${errorMessage}` : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (teams) {
|
||||
const facts = [
|
||||
{ name: "Project", value: projectName },
|
||||
{ name: "Application", value: applicationName },
|
||||
{ name: "Volume Name", value: volumeName },
|
||||
{ name: "Service Type", value: serviceType },
|
||||
{ name: "Date", value: format(date, "PP pp") },
|
||||
{ name: "Status", value: type === "success" ? "Successful" : "Failed" },
|
||||
];
|
||||
if (backupSize) {
|
||||
facts.push({ name: "Backup Size", value: backupSize });
|
||||
}
|
||||
if (type === "error" && errorMessage) {
|
||||
facts.push({ name: "Error", value: errorMessage.substring(0, 500) });
|
||||
}
|
||||
await sendTeamsNotification(teams, {
|
||||
title:
|
||||
type === "success"
|
||||
? "✅ Volume Backup Successful"
|
||||
: "❌ Volume Backup Failed",
|
||||
facts,
|
||||
});
|
||||
}
|
||||
|
||||
if (custom) {
|
||||
await sendCustomNotification(custom, {
|
||||
title: `Volume Backup ${type === "success" ? "Successful" : "Failed"}`,
|
||||
message:
|
||||
type === "success"
|
||||
? "Volume backup completed successfully"
|
||||
: "Volume backup failed",
|
||||
projectName,
|
||||
applicationName,
|
||||
volumeName,
|
||||
serviceType,
|
||||
type,
|
||||
errorMessage: errorMessage ?? "",
|
||||
backupSize: backupSize ?? "",
|
||||
timestamp: date.toISOString(),
|
||||
date: date.toLocaleString(),
|
||||
status: type,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -170,7 +170,7 @@ export const cloneGiteaRepository = async ({
|
||||
|
||||
const repoClone = `${giteaOwner}/${giteaRepository}.git`;
|
||||
const cloneUrl = buildGiteaCloneUrl(
|
||||
giteaProvider.giteaUrl,
|
||||
giteaProvider.giteaInternalUrl || giteaProvider.giteaUrl,
|
||||
giteaProvider.accessToken!,
|
||||
giteaOwner!,
|
||||
giteaRepository!,
|
||||
|
||||
@@ -5,8 +5,8 @@ import { findGithubById, type Github } from "@dokploy/server/services/github";
|
||||
import type { InferResultType } from "@dokploy/server/types/with";
|
||||
import { createAppAuth } from "@octokit/auth-app";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import type { z } from "zod";
|
||||
import { Octokit } from "octokit";
|
||||
import type { z } from "zod";
|
||||
|
||||
export const authGithub = (githubProvider: Github): Octokit => {
|
||||
if (!haveGithubRequirements(githubProvider)) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { join } from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import type { apiGitlabTestConnection } from "@dokploy/server/db/schema";
|
||||
import type { z } from "zod";
|
||||
import {
|
||||
findGitlabById,
|
||||
type Gitlab,
|
||||
@@ -9,6 +8,7 @@ import {
|
||||
} from "@dokploy/server/services/gitlab";
|
||||
import type { InferResultType } from "@dokploy/server/types/with";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import type { z } from "zod";
|
||||
|
||||
export const refreshGitlabToken = async (gitlabProviderId: string) => {
|
||||
const gitlabProvider = await findGitlabById(gitlabProviderId);
|
||||
@@ -90,12 +90,14 @@ const getGitlabRepoClone = (
|
||||
gitlab: GitlabInfo,
|
||||
gitlabPathNamespace: string | null,
|
||||
) => {
|
||||
const repoClone = `${gitlab?.gitlabUrl.replace(/^https?:\/\//, "")}/${gitlabPathNamespace}.git`;
|
||||
const url = gitlab?.gitlabInternalUrl || gitlab?.gitlabUrl;
|
||||
const repoClone = `${url?.replace(/^https?:\/\//, "")}/${gitlabPathNamespace}.git`;
|
||||
return repoClone;
|
||||
};
|
||||
|
||||
const getGitlabCloneUrl = (gitlab: GitlabInfo, repoClone: string) => {
|
||||
const isSecure = gitlab?.gitlabUrl.startsWith("https://");
|
||||
const url = gitlab?.gitlabInternalUrl || gitlab?.gitlabUrl;
|
||||
const isSecure = url?.startsWith("https://");
|
||||
const cloneUrl = `http${isSecure ? "s" : ""}://oauth2:${gitlab?.accessToken}@${repoClone}`;
|
||||
return cloneUrl;
|
||||
};
|
||||
|
||||
@@ -32,7 +32,7 @@ export const restoreComposeBackup = async (
|
||||
rcloneCommand = `rclone copy ${rcloneFlags.join(" ")} "${backupPath}"`;
|
||||
}
|
||||
|
||||
let credentials: DatabaseCredentials;
|
||||
let credentials: DatabaseCredentials = {};
|
||||
|
||||
switch (backupInput.databaseType) {
|
||||
case "postgres":
|
||||
@@ -62,7 +62,11 @@ export const restoreComposeBackup = async (
|
||||
const restoreCommand = getRestoreCommand({
|
||||
appName: appName,
|
||||
serviceName: backupInput.metadata?.serviceName,
|
||||
type: backupInput.databaseType,
|
||||
type: backupInput.databaseType as
|
||||
| "postgres"
|
||||
| "mariadb"
|
||||
| "mysql"
|
||||
| "mongo",
|
||||
credentials: {
|
||||
database: backupInput.databaseName,
|
||||
...credentials,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export { restoreComposeBackup } from "./compose";
|
||||
export { restoreLibsqlBackup } from "./libsql";
|
||||
export { restoreMariadbBackup } from "./mariadb";
|
||||
export { restoreMongoBackup } from "./mongo";
|
||||
export { restoreMySqlBackup } from "./mysql";
|
||||
|
||||
49
packages/server/src/utils/restore/libsql.ts
Normal file
49
packages/server/src/utils/restore/libsql.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
|
||||
import type { Destination } from "@dokploy/server/services/destination";
|
||||
import type { Libsql } from "@dokploy/server/services/libsql";
|
||||
import type { z } from "zod";
|
||||
import { getS3Credentials, getServiceContainerCommand } from "../backups/utils";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
|
||||
export const restoreLibsqlBackup = async (
|
||||
libsql: Libsql,
|
||||
destination: Destination,
|
||||
backupInput: z.infer<typeof apiRestoreBackup>,
|
||||
emit: (log: string) => void,
|
||||
) => {
|
||||
try {
|
||||
const { appName, serverId } = libsql;
|
||||
|
||||
const rcloneFlags = getS3Credentials(destination);
|
||||
const bucketPath = `:s3:${destination.bucket}`;
|
||||
|
||||
const backupPath = `${bucketPath}/${backupInput.backupFile}`;
|
||||
|
||||
const rcloneCommand = `rclone cat ${rcloneFlags.join(" ")} "${backupPath}"`;
|
||||
|
||||
emit("Starting restore...");
|
||||
emit(`Backup path: ${backupPath}`);
|
||||
|
||||
const containerSearch = getServiceContainerCommand(appName);
|
||||
const restoreCommand = `docker exec -i $CONTAINER_ID sh -c "tar xzf - -C /var/lib/sqld"`;
|
||||
|
||||
const command = `CONTAINER_ID=$(${containerSearch}) && ${rcloneCommand} | ${restoreCommand}`;
|
||||
|
||||
emit(`Executing command: ${command}`);
|
||||
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
|
||||
emit("Restore completed successfully!");
|
||||
} catch (error) {
|
||||
emit(
|
||||
`Error: ${
|
||||
error instanceof Error ? error.message : "Error restoring libsql backup"
|
||||
}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -5,7 +5,7 @@ import { paths } from "@dokploy/server/constants";
|
||||
import type { Domain } from "@dokploy/server/services/domain";
|
||||
import { parse, stringify } from "yaml";
|
||||
import { encodeBase64 } from "../docker/utils";
|
||||
import { execAsyncRemote } from "../process/execAsync";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import type { FileConfig, HttpLoadBalancerService } from "./file-types";
|
||||
|
||||
export const createTraefikConfig = (appName: string) => {
|
||||
@@ -57,18 +57,16 @@ export const removeTraefikConfig = async (
|
||||
try {
|
||||
const { DYNAMIC_TRAEFIK_PATH } = paths(!!serverId);
|
||||
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
|
||||
const command = `rm -f ${configPath}`;
|
||||
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, `rm ${configPath}`);
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
if (fs.existsSync(configPath)) {
|
||||
await fs.promises.unlink(configPath);
|
||||
}
|
||||
await execAsync(command);
|
||||
}
|
||||
if (fs.existsSync(configPath)) {
|
||||
await fs.promises.unlink(configPath);
|
||||
}
|
||||
} catch {}
|
||||
} catch (error) {
|
||||
console.error(`Error removing traefik config for ${appName}:`, error);
|
||||
}
|
||||
};
|
||||
|
||||
export const removeTraefikConfigRemote = async (
|
||||
@@ -78,8 +76,13 @@ export const removeTraefikConfigRemote = async (
|
||||
try {
|
||||
const { DYNAMIC_TRAEFIK_PATH } = paths(true);
|
||||
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
|
||||
await execAsyncRemote(serverId, `rm ${configPath}`);
|
||||
} catch {}
|
||||
await execAsyncRemote(serverId, `rm -f ${configPath}`);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error removing remote traefik config for ${appName}:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const loadOrCreateConfig = (appName: string): FileConfig => {
|
||||
|
||||
@@ -32,10 +32,10 @@ export const manageDomain = async (app: ApplicationNested, domain: Domain) => {
|
||||
config.http.routers[routerName] = await createRouterConfig(
|
||||
app,
|
||||
domain,
|
||||
"web",
|
||||
domain.customEntrypoint || "web",
|
||||
);
|
||||
|
||||
if (domain.https) {
|
||||
if (!domain.customEntrypoint && domain.https) {
|
||||
config.http.routers[routerNameSecure] = await createRouterConfig(
|
||||
app,
|
||||
domain,
|
||||
@@ -121,13 +121,20 @@ const toPunycode = (host: string): string => {
|
||||
export const createRouterConfig = async (
|
||||
app: ApplicationNested,
|
||||
domain: Domain,
|
||||
entryPoint: "web" | "websecure",
|
||||
entryPoint: string,
|
||||
) => {
|
||||
const { appName, redirects, security } = app;
|
||||
const { certificateType } = domain;
|
||||
|
||||
const { host, path, https, uniqueConfigKey, internalPath, stripPath } =
|
||||
domain;
|
||||
const {
|
||||
host,
|
||||
path,
|
||||
https,
|
||||
uniqueConfigKey,
|
||||
internalPath,
|
||||
stripPath,
|
||||
customEntrypoint,
|
||||
} = domain;
|
||||
const punycodeHost = toPunycode(host);
|
||||
const routerConfig: HttpRouter = {
|
||||
rule: `Host(\`${punycodeHost}\`)${path !== null && path !== "/" ? ` && PathPrefix(\`${path}\`)` : ""}`,
|
||||
@@ -136,22 +143,24 @@ export const createRouterConfig = async (
|
||||
entryPoints: [entryPoint],
|
||||
};
|
||||
|
||||
// Add path rewriting middleware if needed
|
||||
if (internalPath && internalPath !== "/" && internalPath !== path) {
|
||||
const pathMiddleware = `addprefix-${appName}-${uniqueConfigKey}`;
|
||||
routerConfig.middlewares?.push(pathMiddleware);
|
||||
}
|
||||
const isRedirectRouter = entryPoint === "web" && https && !customEntrypoint;
|
||||
|
||||
if (stripPath && path && path !== "/") {
|
||||
const stripMiddleware = `stripprefix-${appName}-${uniqueConfigKey}`;
|
||||
routerConfig.middlewares?.push(stripMiddleware);
|
||||
}
|
||||
// Web router with HTTPS only needs redirect — all other middlewares
|
||||
// run on the websecure router where the request actually lands.
|
||||
if (isRedirectRouter) {
|
||||
routerConfig.middlewares?.push("redirect-to-https");
|
||||
} else {
|
||||
// Add path rewriting middleware if needed
|
||||
if (internalPath && internalPath !== "/" && internalPath !== path) {
|
||||
const pathMiddleware = `addprefix-${appName}-${uniqueConfigKey}`;
|
||||
routerConfig.middlewares?.push(pathMiddleware);
|
||||
}
|
||||
|
||||
if (entryPoint === "web" && https) {
|
||||
routerConfig.middlewares = ["redirect-to-https"];
|
||||
}
|
||||
if (stripPath && path && path !== "/") {
|
||||
const stripMiddleware = `stripprefix-${appName}-${uniqueConfigKey}`;
|
||||
routerConfig.middlewares?.push(stripMiddleware);
|
||||
}
|
||||
|
||||
if ((entryPoint === "websecure" && https) || !https) {
|
||||
// redirects - skip for preview deployments as wildcard subdomains
|
||||
// should not inherit parent redirect rules (e.g., www-redirect)
|
||||
if (domain.domainType !== "preview") {
|
||||
@@ -172,9 +181,14 @@ export const createRouterConfig = async (
|
||||
}
|
||||
routerConfig.middlewares?.push(middlewareName);
|
||||
}
|
||||
|
||||
// custom middlewares from domain
|
||||
if (domain.middlewares && domain.middlewares.length > 0) {
|
||||
routerConfig.middlewares?.push(...domain.middlewares);
|
||||
}
|
||||
}
|
||||
|
||||
if (entryPoint === "websecure") {
|
||||
if (entryPoint === "websecure" || (customEntrypoint && https)) {
|
||||
if (certificateType === "letsencrypt") {
|
||||
routerConfig.tls = { certResolver: "letsencrypt" };
|
||||
} else if (certificateType === "custom" && domain.customCertResolver) {
|
||||
|
||||
@@ -2,7 +2,11 @@ import path from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import { findComposeById } from "@dokploy/server/services/compose";
|
||||
import type { findVolumeBackupById } from "@dokploy/server/services/volume-backups";
|
||||
import { getS3Credentials, normalizeS3Path } from "../backups/utils";
|
||||
import {
|
||||
getBackupTimestamp,
|
||||
getS3Credentials,
|
||||
normalizeS3Path,
|
||||
} from "../backups/utils";
|
||||
|
||||
export const getVolumeServiceAppName = (
|
||||
volumeBackup: Awaited<ReturnType<typeof findVolumeBackupById>>,
|
||||
@@ -18,7 +22,8 @@ export const getVolumeServiceAppName = (
|
||||
volumeBackup.mysql?.appName ||
|
||||
volumeBackup.mariadb?.appName ||
|
||||
volumeBackup.mongo?.appName ||
|
||||
volumeBackup.redis?.appName;
|
||||
volumeBackup.redis?.appName ||
|
||||
volumeBackup.libsql?.appName;
|
||||
return serviceAppName || volumeBackup.appName;
|
||||
};
|
||||
|
||||
@@ -31,7 +36,7 @@ export const backupVolume = async (
|
||||
const { VOLUME_BACKUPS_PATH, VOLUME_BACKUP_LOCK_PATH } = paths(!!serverId);
|
||||
const destination = volumeBackup.destination;
|
||||
const s3AppName = getVolumeServiceAppName(volumeBackup);
|
||||
const backupFileName = `${volumeName}-${new Date().toISOString()}.tar`;
|
||||
const backupFileName = `${volumeName}-${getBackupTimestamp()}.tar`;
|
||||
const bucketDestination = `${s3AppName}/${normalizeS3Path(prefix || "")}${backupFileName}`;
|
||||
const rcloneFlags = getS3Credentials(volumeBackup.destination);
|
||||
const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`;
|
||||
|
||||
@@ -26,6 +26,7 @@ const getProjectName = (
|
||||
volumeBackup.mariadb,
|
||||
volumeBackup.mongo,
|
||||
volumeBackup.redis,
|
||||
volumeBackup.libsql,
|
||||
];
|
||||
|
||||
for (const service of services) {
|
||||
@@ -48,6 +49,7 @@ const getOrganizationId = (
|
||||
volumeBackup.mariadb,
|
||||
volumeBackup.mongo,
|
||||
volumeBackup.redis,
|
||||
volumeBackup.libsql,
|
||||
];
|
||||
|
||||
for (const service of services) {
|
||||
|
||||
Reference in New Issue
Block a user