mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-10-15 13:21:11 +00:00
Merge branch 'canary' into feat/add-terminal-permission
This commit is contained in:
@@ -21,6 +21,7 @@ export const network = pgTable("network", {
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
name: text("name").notNull(),
|
||||
dockerId: text("dockerId"),
|
||||
driver: networkDriver("driver").notNull().default("bridge"),
|
||||
internal: boolean("internal").notNull().default(false),
|
||||
attachable: boolean("attachable").notNull().default(false),
|
||||
|
||||
@@ -262,18 +262,28 @@ export const updateFileMount = async (mountId: string) => {
|
||||
if (!mount || !mount.filePath) return;
|
||||
const basePath = await getBaseFilesPath(mountId);
|
||||
const fullPath = path.join(basePath, mount.filePath);
|
||||
const directory = path.dirname(fullPath);
|
||||
|
||||
try {
|
||||
const serverId = await getServerId(mount);
|
||||
const encodedContent = encodeBase64(mount.content || "");
|
||||
const command = `echo "${encodedContent}" | base64 -d > ${quote([fullPath])}`;
|
||||
const command = `
|
||||
mkdir -p ${quote([directory])};
|
||||
if [ -d ${quote([fullPath])} ]; then rm -rf ${quote([fullPath])}; fi;
|
||||
echo "${encodedContent}" | base64 -d > ${quote([fullPath])};
|
||||
`;
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
} catch {
|
||||
console.log("Error updating file mount");
|
||||
} catch (error) {
|
||||
console.log(`Error updating the file mount: ${error}`);
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Error updating the mount ${error instanceof Error ? error.message : error}`,
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { type apiCreateNetwork, network } from "@dokploy/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import type Dockerode from "dockerode";
|
||||
import { and, eq, inArray, isNull } from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
import { IS_CLOUD } from "../constants";
|
||||
@@ -17,6 +18,7 @@ const RESERVED_NETWORKS = [
|
||||
];
|
||||
|
||||
type DockerNetworkInfo = {
|
||||
Id?: string;
|
||||
Name: string;
|
||||
Driver: string;
|
||||
Internal?: boolean;
|
||||
@@ -35,6 +37,12 @@ type DockerNetworkInfo = {
|
||||
};
|
||||
};
|
||||
|
||||
// EnableIPv4 is missing from dockerode's NetworkCreateOptions but supported
|
||||
// by the daemon (API >= 1.47); the body is sent as-is
|
||||
type NetworkCreateOptions = Dockerode.NetworkCreateOptions & {
|
||||
EnableIPv4?: boolean;
|
||||
};
|
||||
|
||||
const parseMtu = (value: string | undefined) => {
|
||||
const mtu = Number.parseInt(value ?? "", 10);
|
||||
return Number.isNaN(mtu) ? null : mtu;
|
||||
@@ -51,6 +59,7 @@ const mapDockerNetworkToRow = (
|
||||
serverId: string | null,
|
||||
) => ({
|
||||
name: dockerNetwork.Name,
|
||||
dockerId: dockerNetwork.Id ?? null,
|
||||
driver: dockerNetwork.Driver as "bridge" | "overlay",
|
||||
internal: dockerNetwork.Internal ?? false,
|
||||
attachable: dockerNetwork.Attachable ?? false,
|
||||
@@ -110,7 +119,7 @@ export const findNetworksToSync = async (
|
||||
|
||||
const existing = await findNetworksByServer(organizationId, serverId);
|
||||
const existingNames = new Set(existing.map((row) => row.name));
|
||||
const dockerNames = new Set(dockerNetworks.map((d) => d.Name));
|
||||
const dockerByName = new Map(dockerNetworks.map((d) => [d.Name, d] as const));
|
||||
|
||||
const importable = dockerNetworks
|
||||
.filter(
|
||||
@@ -128,12 +137,28 @@ export const findNetworksToSync = async (
|
||||
.filter((s): s is string => !!s),
|
||||
}));
|
||||
|
||||
// Rows in Dokploy whose network no longer exists in Docker
|
||||
const missing = existing
|
||||
.filter((row) => !dockerNames.has(row.name))
|
||||
.filter((row) => !dockerByName.has(row.name))
|
||||
.map((row) => ({ networkId: row.networkId, name: row.name }));
|
||||
|
||||
return { importable, missing };
|
||||
const changed = existing
|
||||
.filter((row) => {
|
||||
if (!row.dockerId) return false;
|
||||
const dockerNetwork = dockerByName.get(row.name);
|
||||
return !!dockerNetwork?.Id && dockerNetwork.Id !== row.dockerId;
|
||||
})
|
||||
.map((row) => {
|
||||
const dockerNetwork = dockerByName.get(row.name);
|
||||
return {
|
||||
networkId: row.networkId,
|
||||
name: row.name,
|
||||
driver: dockerNetwork?.Driver,
|
||||
internal: dockerNetwork?.Internal ?? false,
|
||||
attachable: dockerNetwork?.Attachable ?? false,
|
||||
};
|
||||
});
|
||||
|
||||
return { importable, missing, changed };
|
||||
};
|
||||
|
||||
export const importDockerNetworks = async (
|
||||
@@ -184,6 +209,49 @@ export const importDockerNetworks = async (
|
||||
return { imported, errors };
|
||||
};
|
||||
|
||||
export const resyncNetwork = async (
|
||||
networkId: string,
|
||||
organizationId: string,
|
||||
) => {
|
||||
const row = await findNetworkById(networkId);
|
||||
if (row.organizationId !== organizationId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Network not found",
|
||||
});
|
||||
}
|
||||
|
||||
const docker = await getRemoteDocker(row.serverId ?? null);
|
||||
let info: DockerNetworkInfo;
|
||||
try {
|
||||
info = (await docker.getNetwork(row.name).inspect()) as DockerNetworkInfo;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to inspect Docker network",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
|
||||
const [updated] = await db
|
||||
.update(network)
|
||||
.set(mapDockerNetworkToRow(info, organizationId, row.serverId))
|
||||
.where(eq(network.networkId, networkId))
|
||||
.returning();
|
||||
|
||||
if (!updated) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Network not found",
|
||||
});
|
||||
}
|
||||
|
||||
return updated;
|
||||
};
|
||||
|
||||
export const findNetworkById = async (networkId: string) => {
|
||||
const [row] = await db
|
||||
.select()
|
||||
@@ -252,14 +320,12 @@ const createDockerNetworkFromRow = async (row: typeof network.$inferSelect) => {
|
||||
|
||||
const docker = await getRemoteDocker(row.serverId ?? null);
|
||||
try {
|
||||
await docker.createNetwork({
|
||||
const createOptions: NetworkCreateOptions = {
|
||||
Name: row.name,
|
||||
Driver: row.driver,
|
||||
CheckDuplicate: true,
|
||||
Internal: row.internal,
|
||||
Attachable: row.attachable,
|
||||
// EnableIPv4 is missing from dockerode's types but supported by
|
||||
// the daemon (API >= 1.47); the body is sent as-is
|
||||
EnableIPv4: row.enableIPv4,
|
||||
EnableIPv6: row.enableIPv6,
|
||||
Options: row.mtu
|
||||
@@ -269,7 +335,13 @@ const createDockerNetworkFromRow = async (row: typeof network.$inferSelect) => {
|
||||
Driver: ipam.driver || "default",
|
||||
Config: ipamConfig.length > 0 ? ipamConfig : undefined,
|
||||
},
|
||||
} as Parameters<typeof docker.createNetwork>[0]);
|
||||
};
|
||||
const created = await docker.createNetwork(createOptions);
|
||||
|
||||
await db
|
||||
.update(network)
|
||||
.set({ dockerId: created.id })
|
||||
.where(eq(network.networkId, row.networkId));
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
|
||||
@@ -485,7 +485,7 @@ export const reconnectServicesToTraefik = async (serverId?: string) => {
|
||||
),
|
||||
});
|
||||
|
||||
if (!composeResult) {
|
||||
if (composeResult.length === 0) {
|
||||
return;
|
||||
}
|
||||
let commands = "";
|
||||
|
||||
@@ -21,11 +21,31 @@ const getMonitoringImage = () => {
|
||||
return imageName;
|
||||
};
|
||||
|
||||
// Swarm tasks are dokploy-monitoring.<slot>.<id>, so this only matches the
|
||||
// pre-v0.30.0 standalone container. A cleanup failure must not block the deploy.
|
||||
const removeLegacyContainer = async (
|
||||
docker: Awaited<ReturnType<typeof getRemoteDocker>>,
|
||||
serviceName: string,
|
||||
) => {
|
||||
try {
|
||||
await docker.getContainer(serviceName).remove({ force: true });
|
||||
console.log("Removed legacy monitoring container ✅");
|
||||
} catch (error: any) {
|
||||
if (error?.statusCode !== 404) {
|
||||
console.warn(
|
||||
`Could not remove legacy monitoring container: ${error?.message ?? error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const deployMonitoringService = async (
|
||||
docker: Awaited<ReturnType<typeof getRemoteDocker>>,
|
||||
serviceName: string,
|
||||
settings: CreateServiceOptions,
|
||||
) => {
|
||||
await removeLegacyContainer(docker, serviceName);
|
||||
|
||||
try {
|
||||
const service = docker.getService(serviceName);
|
||||
const inspect = await service.inspect();
|
||||
|
||||
@@ -86,6 +86,7 @@ export const serverSetup = async (
|
||||
...server.metricsConfig.server,
|
||||
token: token,
|
||||
urlCallback: urlCallback,
|
||||
cronJob: server.metricsConfig.server.cronJob || "0 0 * * *",
|
||||
},
|
||||
containers: server.metricsConfig.containers,
|
||||
},
|
||||
|
||||
@@ -21,11 +21,11 @@ export const getBuildComposeCommand = async (rawCompose: ComposeNested) => {
|
||||
const compose = await withResolvedVaultRefs(rawCompose);
|
||||
const { COMPOSE_PATH } = paths(!!compose.serverId);
|
||||
const { sourceType, appName, mounts, composeType, domains } = compose;
|
||||
const command = createCommand(compose);
|
||||
const projectPath = join(COMPOSE_PATH, compose.appName, "code");
|
||||
const command = createCommand(compose, projectPath);
|
||||
const envCommand = compose.createEnvFile
|
||||
? getCreateEnvFileCommand(compose)
|
||||
: "";
|
||||
const projectPath = join(COMPOSE_PATH, compose.appName, "code");
|
||||
const exportEnvCommand = getExportEnvCommand(compose);
|
||||
|
||||
const newCompose = await writeDomainsToCompose(compose, domains);
|
||||
@@ -119,7 +119,7 @@ const sanitizeCommand = (command: string) => {
|
||||
return restCommand.join(" ");
|
||||
};
|
||||
|
||||
export const createCommand = (compose: ComposeNested) => {
|
||||
export const createCommand = (compose: ComposeNested, projectPath?: string) => {
|
||||
const { composeType, appName, sourceType } = compose;
|
||||
if (compose.command) {
|
||||
return `${sanitizeCommand(compose.command)}`;
|
||||
@@ -130,7 +130,10 @@ export const createCommand = (compose: ComposeNested) => {
|
||||
let command = "";
|
||||
|
||||
if (composeType === "docker-compose") {
|
||||
command = `compose -p ${quote([appName])} -f ${quote([path])} up -d --build --remove-orphans`;
|
||||
const projectDirectoryFlag = projectPath
|
||||
? `--project-directory ${quote([projectPath])} `
|
||||
: "";
|
||||
command = `compose -p ${quote([appName])} ${projectDirectoryFlag}-f ${quote([path])} up -d --build --remove-orphans`;
|
||||
} else if (composeType === "stack") {
|
||||
command = `stack deploy -c ${quote([path])} ${quote([appName])} --prune --with-registry-auth`;
|
||||
}
|
||||
|
||||
@@ -87,7 +87,15 @@ export const getRailpackCommand = (application: ApplicationNested) => {
|
||||
# Ensure we have a builder with containerd (isolated per build)
|
||||
|
||||
export RAILPACK_VERSION=${application.railpackVersion}
|
||||
bash -c "$(curl -fsSL https://railpack.com/install.sh)"
|
||||
# use sudo for non-root so the install can write to /usr/local/bin
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
SUDO_CMD=""
|
||||
elif sudo -n true 2>/dev/null; then
|
||||
SUDO_CMD="sudo"
|
||||
else
|
||||
SUDO_CMD=""
|
||||
fi
|
||||
$SUDO_CMD bash -c "$(curl -fsSL https://railpack.com/install.sh)"
|
||||
docker buildx create --name ${builderName} --driver docker-container || true
|
||||
|
||||
echo "Preparing Railpack build plan..." ;
|
||||
|
||||
@@ -548,7 +548,7 @@ export const prepareEnvironmentVariablesForFile = (
|
||||
const escapedValue = value
|
||||
.replace(/\\/g, "\\\\")
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/\$/g, "\\$");
|
||||
.replace(/\$(?!\{[A-Za-z_][A-Za-z0-9_]*(?::?[-+?][^{}]*)?\})/g, "\\$");
|
||||
return `${key}="${escapedValue}"`;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -292,6 +292,26 @@ export const writeTraefikConfigRemote = async (
|
||||
}
|
||||
};
|
||||
|
||||
const isEmptyHttpRoutersAndServices = (traefikConfig: FileConfig) =>
|
||||
Object.keys(traefikConfig.http?.routers || {}).length === 0 &&
|
||||
Object.keys(traefikConfig.http?.services || {}).length === 0;
|
||||
|
||||
export const writeAppTraefikConfig = async (
|
||||
traefikConfig: FileConfig,
|
||||
appName: string,
|
||||
serverId?: string | null,
|
||||
) => {
|
||||
if (isEmptyHttpRoutersAndServices(traefikConfig)) {
|
||||
await removeTraefikConfig(appName, serverId);
|
||||
return;
|
||||
}
|
||||
if (serverId) {
|
||||
await writeTraefikConfigRemote(traefikConfig, appName, serverId);
|
||||
} else {
|
||||
writeTraefikConfig(traefikConfig, appName);
|
||||
}
|
||||
};
|
||||
|
||||
export const createServiceConfig = (
|
||||
appName: string,
|
||||
domain: Domain,
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { ApplicationNested } from "../builders";
|
||||
import {
|
||||
loadOrCreateConfig,
|
||||
loadOrCreateConfigRemote,
|
||||
writeTraefikConfig,
|
||||
writeAppTraefikConfig,
|
||||
writeTraefikConfigRemote,
|
||||
} from "./application";
|
||||
import type { FileConfig } from "./file-types";
|
||||
@@ -89,11 +89,10 @@ export const createRedirectMiddleware = async (
|
||||
|
||||
if (serverId) {
|
||||
await writeTraefikConfigRemote(config, "middlewares", serverId);
|
||||
await writeTraefikConfigRemote(appConfig, appName, serverId);
|
||||
} else {
|
||||
writeMiddleware(config);
|
||||
writeTraefikConfig(appConfig, appName);
|
||||
}
|
||||
await writeAppTraefikConfig(appConfig, appName, serverId);
|
||||
};
|
||||
|
||||
export const removeRedirectMiddleware = async (
|
||||
@@ -124,9 +123,8 @@ export const removeRedirectMiddleware = async (
|
||||
|
||||
if (serverId) {
|
||||
await writeTraefikConfigRemote(config, "middlewares", serverId);
|
||||
await writeTraefikConfigRemote(appConfig, appName, serverId);
|
||||
} else {
|
||||
writeTraefikConfig(appConfig, appName);
|
||||
writeMiddleware(config);
|
||||
}
|
||||
await writeAppTraefikConfig(appConfig, appName, serverId);
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { ApplicationNested } from "../builders";
|
||||
import {
|
||||
loadOrCreateConfig,
|
||||
loadOrCreateConfigRemote,
|
||||
writeTraefikConfig,
|
||||
writeAppTraefikConfig,
|
||||
writeTraefikConfigRemote,
|
||||
} from "./application";
|
||||
import type {
|
||||
@@ -62,11 +62,10 @@ export const createSecurityMiddleware = async (
|
||||
addMiddleware(appConfig, middlewareName);
|
||||
if (serverId) {
|
||||
await writeTraefikConfigRemote(config, "middlewares", serverId);
|
||||
await writeTraefikConfigRemote(appConfig, appName, serverId);
|
||||
} else {
|
||||
writeTraefikConfig(appConfig, appName);
|
||||
writeMiddleware(config);
|
||||
}
|
||||
await writeAppTraefikConfig(appConfig, appName, serverId);
|
||||
};
|
||||
|
||||
export const removeSecurityMiddleware = async (
|
||||
@@ -89,6 +88,7 @@ export const removeSecurityMiddleware = async (
|
||||
appConfig = loadOrCreateConfig(appName);
|
||||
}
|
||||
const middlewareName = `auth-${appName}`;
|
||||
let removedLastUser = false;
|
||||
|
||||
if (config.http?.middlewares) {
|
||||
const currentMiddleware = config.http.middlewares[middlewareName];
|
||||
@@ -106,11 +106,7 @@ export const removeSecurityMiddleware = async (
|
||||
delete config.http.middlewares[middlewareName];
|
||||
}
|
||||
deleteMiddleware(appConfig, middlewareName);
|
||||
if (serverId) {
|
||||
await writeTraefikConfigRemote(appConfig, appName, serverId);
|
||||
} else {
|
||||
writeTraefikConfig(appConfig, appName);
|
||||
}
|
||||
removedLastUser = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,6 +116,9 @@ export const removeSecurityMiddleware = async (
|
||||
} else {
|
||||
writeMiddleware(config);
|
||||
}
|
||||
if (removedLastUser) {
|
||||
await writeAppTraefikConfig(appConfig, appName, serverId);
|
||||
}
|
||||
};
|
||||
|
||||
const isBasicAuthMiddleware = (
|
||||
|
||||
@@ -40,7 +40,7 @@ export const readValidDirectory = (
|
||||
directory: string,
|
||||
serverId?: string | null,
|
||||
) => {
|
||||
if (!/^[\w/. :[\]-]{1,500}$/.test(directory)) {
|
||||
if (!/^[\w/. :[\]+@~(),=%-]{1,500}$/.test(directory)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user