mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-10-23 08:55:25 +00:00
feat: enhance log management and organization handling
- Updated API link conditions to allow skipping batch processing based on context. - Removed unused VECTOR_RESYNC_CRON_JOB constant from server constants. - Introduced logProviderIds field in server and webServerSettings schemas to manage log providers. - Added organization service to handle organization deletions and vector agent removals. - Enhanced server service to find servers with log management enabled and update log providers. - Updated vector setup to sync vector configurations based on organization and server log provider IDs. - Implemented Docker labels for resources to improve tracking and management. - Refactored vector agent setup and removal processes to streamline operations.
This commit is contained in:
@@ -13,8 +13,6 @@ export const DOKPLOY_DOCKER_PORT = process.env.DOKPLOY_DOCKER_PORT
|
||||
|
||||
export const CLEANUP_CRON_JOB = "50 23 * * *";
|
||||
|
||||
export const VECTOR_RESYNC_CRON_JOB = "*/15 * * * *";
|
||||
|
||||
// Body size limits for the OpenAPI catch-all route (pages/api/[...trpc].ts).
|
||||
const parseByteSize = (envVar: string, fallback: number): number => {
|
||||
const raw = process.env[envVar];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { relations, sql } from "drizzle-orm";
|
||||
import {
|
||||
boolean,
|
||||
integer,
|
||||
@@ -42,7 +42,10 @@ export const server = pgTable("server", {
|
||||
.notNull()
|
||||
.$defaultFn(() => generateAppName("server")),
|
||||
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(false),
|
||||
enableLogManagement: boolean("enableLogManagement").notNull().default(false),
|
||||
logProviderIds: text("logProviderIds")
|
||||
.array()
|
||||
.notNull()
|
||||
.default(sql`ARRAY[]::text[]`),
|
||||
buildsConcurrency: integer("buildsConcurrency").notNull().default(1),
|
||||
createdAt: text("createdAt").notNull(),
|
||||
organizationId: text("organizationId")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { relations, sql } from "drizzle-orm";
|
||||
import {
|
||||
boolean,
|
||||
integer,
|
||||
@@ -27,7 +27,10 @@ export const webServerSettings = pgTable("webServerSettings", {
|
||||
sshPrivateKey: text("sshPrivateKey"),
|
||||
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(true),
|
||||
logCleanupCron: text("logCleanupCron").default("0 0 * * *"),
|
||||
enableLogManagement: boolean("enableLogManagement").notNull().default(false),
|
||||
logProviderIds: text("logProviderIds")
|
||||
.array()
|
||||
.notNull()
|
||||
.default(sql`ARRAY[]::text[]`),
|
||||
logManagementOrganizationId: text("logManagementOrganizationId").references(
|
||||
() => organization.id,
|
||||
{ onDelete: "set null" },
|
||||
|
||||
@@ -36,6 +36,7 @@ export * from "./services/mount";
|
||||
export * from "./services/mysql";
|
||||
export * from "./services/network";
|
||||
export * from "./services/notification";
|
||||
export * from "./services/organization";
|
||||
export * from "./services/overview";
|
||||
export * from "./services/patch";
|
||||
export * from "./services/patch-repo";
|
||||
|
||||
@@ -117,6 +117,7 @@ export const elasticsearchAdapter: LogProviderAdapter = {
|
||||
type: "elasticsearch",
|
||||
inputs: [inputId],
|
||||
endpoints: [normalizeEndpointUrl(config.endpoint ?? "")],
|
||||
api_version: "v8",
|
||||
...(typeof index === "string" && index.length > 0
|
||||
? { bulk: { index } }
|
||||
: {}),
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { organization } from "@dokploy/server/db/schema";
|
||||
import { findServersWithLogManagementEnabled } from "@dokploy/server/services/server";
|
||||
import { removeVectorAgent } from "@dokploy/server/setup/vector-setup";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
const removeVectorAgents = async (organizationId: string) => {
|
||||
const servers = await findServersWithLogManagementEnabled(organizationId);
|
||||
await Promise.all(
|
||||
servers.map((server) =>
|
||||
removeVectorAgent(server.serverId).catch((error) => {
|
||||
console.error(
|
||||
`[Vector] Failed to remove the agent on server ${server.serverId} before deleting organization ${organizationId}:`,
|
||||
error,
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
export const deleteOrganization = async (organizationId: string) => {
|
||||
await removeVectorAgents(organizationId);
|
||||
return await db
|
||||
.delete(organization)
|
||||
.where(eq(organization.id, organizationId));
|
||||
};
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { and, desc, eq, isNotNull, sql } from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
|
||||
export type Server = typeof server.$inferSelect;
|
||||
@@ -93,19 +93,43 @@ export const findServersWithLogManagementEnabled = async (
|
||||
return await db.query.server.findMany({
|
||||
where: and(
|
||||
eq(server.organizationId, organizationId),
|
||||
eq(server.enableLogManagement, true),
|
||||
sql`array_length(${server.logProviderIds}, 1) > 0`,
|
||||
),
|
||||
columns: { serverId: true },
|
||||
});
|
||||
};
|
||||
|
||||
export const findAllServersWithLogManagementEnabled = async () => {
|
||||
export const findServersByOrganizationForLogManagement = async (
|
||||
organizationId: string,
|
||||
) => {
|
||||
return await db.query.server.findMany({
|
||||
where: eq(server.enableLogManagement, true),
|
||||
columns: { serverId: true, organizationId: true },
|
||||
where: and(
|
||||
eq(server.organizationId, organizationId),
|
||||
isNotNull(server.sshKeyId),
|
||||
eq(server.serverType, "deploy"),
|
||||
),
|
||||
columns: {
|
||||
serverId: true,
|
||||
name: true,
|
||||
ipAddress: true,
|
||||
logProviderIds: true,
|
||||
},
|
||||
orderBy: [desc(server.createdAt)],
|
||||
});
|
||||
};
|
||||
|
||||
export const updateServerLogProviders = async (
|
||||
serverId: string,
|
||||
logProviderIds: string[],
|
||||
) => {
|
||||
const [updated] = await db
|
||||
.update(server)
|
||||
.set({ logProviderIds })
|
||||
.where(eq(server.serverId, serverId))
|
||||
.returning();
|
||||
return updated ?? null;
|
||||
};
|
||||
|
||||
export const deleteServer = async (serverId: string) => {
|
||||
const currentServer = await db
|
||||
.delete(server)
|
||||
|
||||
@@ -45,7 +45,7 @@ export const updateWebServerSettings = async (
|
||||
|
||||
export const claimWebServerLogManagement = async (
|
||||
organizationId: string,
|
||||
enableLogManagement: boolean,
|
||||
logProviderIds: string[],
|
||||
) => {
|
||||
const current = await getWebServerSettings();
|
||||
if (!current) {
|
||||
@@ -55,8 +55,9 @@ export const claimWebServerLogManagement = async (
|
||||
const [updated] = await db
|
||||
.update(webServerSettings)
|
||||
.set({
|
||||
enableLogManagement,
|
||||
logManagementOrganizationId: enableLogManagement ? organizationId : null,
|
||||
logProviderIds,
|
||||
logManagementOrganizationId:
|
||||
logProviderIds.length > 0 ? organizationId : null,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(
|
||||
|
||||
@@ -1,33 +1,24 @@
|
||||
import {
|
||||
chmodSync as fsChmodSync,
|
||||
mkdirSync,
|
||||
renameSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { dbUrl } from "@dokploy/server/db/constants";
|
||||
import { projects } from "@dokploy/server/db/schema";
|
||||
import { IS_CLOUD, paths } from "@dokploy/server/constants";
|
||||
import {
|
||||
findEnabledLogProvidersByOrganization,
|
||||
hasEnabledLogProvider,
|
||||
type LogProvider,
|
||||
toRuntimeConfig,
|
||||
} from "@dokploy/server/services/log-management/service";
|
||||
import { findServerById } from "@dokploy/server/services/server";
|
||||
import {
|
||||
findServerById,
|
||||
findServersByOrganizationForLogManagement,
|
||||
} from "@dokploy/server/services/server";
|
||||
import { getWebServerSettings } from "@dokploy/server/services/web-server-settings";
|
||||
import { pullImage, pullRemoteImage } from "@dokploy/server/utils/docker/utils";
|
||||
import { encodeBase64 } from "@dokploy/server/utils/docker/utils";
|
||||
import { ExecError } from "@dokploy/server/utils/process/ExecError";
|
||||
import {
|
||||
execAsync,
|
||||
execAsyncRemote,
|
||||
writeFileRemote,
|
||||
} from "@dokploy/server/utils/process/execAsync";
|
||||
import { getRemoteDocker } from "@dokploy/server/utils/servers/remote-docker";
|
||||
import type { CreateServiceOptions } from "dockerode";
|
||||
import { eq } from "drizzle-orm";
|
||||
import postgres from "postgres";
|
||||
import { nanoid } from "nanoid";
|
||||
import { quote } from "shell-quote";
|
||||
import { stringify } from "yaml";
|
||||
import { getLogProviderAdapter } from "../services/log-management/providers/registry";
|
||||
import type {
|
||||
@@ -41,124 +32,17 @@ const VECTOR_CONFIG_CONTAINER_DIR = "/etc/vector";
|
||||
const VECTOR_CONFIG_CONTAINER_PATH = `${VECTOR_CONFIG_CONTAINER_DIR}/vector.yaml`;
|
||||
const VECTOR_DATA_DIR_CONTAINER = "/var/lib/vector";
|
||||
|
||||
interface AppNameLookupEntry {
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
environmentId: string;
|
||||
environmentName: string;
|
||||
applicationId: string;
|
||||
applicationName: string;
|
||||
}
|
||||
|
||||
const escapeVrlString = (value: string): string =>
|
||||
value
|
||||
.replace(/\\/g, "\\\\")
|
||||
.replace(/"/g, '\\"')
|
||||
// biome-ignore lint/suspicious/noControlCharactersInRegex: replacing them is the point
|
||||
.replace(/[\u0000-\u001f\u007f]/g, " ");
|
||||
|
||||
const buildAppNameLookup = async (
|
||||
organizationId: string,
|
||||
): Promise<Record<string, AppNameLookupEntry>> => {
|
||||
const projectRows = await db.query.projects.findMany({
|
||||
where: eq(projects.organizationId, organizationId),
|
||||
columns: { projectId: true, name: true },
|
||||
with: {
|
||||
environments: {
|
||||
columns: { environmentId: true, name: true },
|
||||
with: {
|
||||
applications: {
|
||||
columns: { applicationId: true, appName: true, name: true },
|
||||
},
|
||||
compose: {
|
||||
columns: { composeId: true, appName: true, name: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const lookup: Record<string, AppNameLookupEntry> = {};
|
||||
for (const project of projectRows) {
|
||||
for (const environment of project.environments) {
|
||||
for (const application of environment.applications) {
|
||||
lookup[application.appName] = {
|
||||
projectId: project.projectId,
|
||||
projectName: project.name,
|
||||
environmentId: environment.environmentId,
|
||||
environmentName: environment.name,
|
||||
applicationId: application.applicationId,
|
||||
applicationName: application.name,
|
||||
};
|
||||
}
|
||||
for (const composeService of environment.compose) {
|
||||
lookup[composeService.appName] = {
|
||||
projectId: project.projectId,
|
||||
projectName: project.name,
|
||||
environmentId: environment.environmentId,
|
||||
environmentName: environment.name,
|
||||
applicationId: composeService.composeId,
|
||||
applicationName: composeService.name,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return lookup;
|
||||
};
|
||||
|
||||
const buildScopeTransformSource = (
|
||||
organizationId: string,
|
||||
lookup: Record<string, AppNameLookupEntry>,
|
||||
): string => {
|
||||
const lines: string[] = [
|
||||
`.dokploy_organization = "${escapeVrlString(organizationId)}"`,
|
||||
'.dokploy_project = ""',
|
||||
'.dokploy_project_id = ""',
|
||||
'.dokploy_environment = ""',
|
||||
'.dokploy_environment_id = ""',
|
||||
'.dokploy_application = ""',
|
||||
'.dokploy_application_id = ""',
|
||||
];
|
||||
|
||||
const entries = Object.entries(lookup);
|
||||
if (entries.length === 0) {
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
lines.push('app_name = .label."com.docker.compose.project"');
|
||||
lines.push("if app_name == null {");
|
||||
lines.push(' app_name = .label."com.docker.stack.namespace"');
|
||||
lines.push("}");
|
||||
lines.push("if app_name == null {");
|
||||
lines.push(' app_name = .label."com.docker.swarm.service.name"');
|
||||
lines.push("}");
|
||||
lines.push("if app_name != null {");
|
||||
entries.forEach(([appName, entry], index) => {
|
||||
const branch = index === 0 ? " if" : " } else if";
|
||||
lines.push(`${branch} app_name == "${escapeVrlString(appName)}" {`);
|
||||
lines.push(
|
||||
` .dokploy_project = "${escapeVrlString(entry.projectName)}"`,
|
||||
);
|
||||
lines.push(
|
||||
` .dokploy_project_id = "${escapeVrlString(entry.projectId)}"`,
|
||||
);
|
||||
lines.push(
|
||||
` .dokploy_environment = "${escapeVrlString(entry.environmentName)}"`,
|
||||
);
|
||||
lines.push(
|
||||
` .dokploy_environment_id = "${escapeVrlString(entry.environmentId)}"`,
|
||||
);
|
||||
lines.push(
|
||||
` .dokploy_application = "${escapeVrlString(entry.applicationName)}"`,
|
||||
);
|
||||
lines.push(
|
||||
` .dokploy_application_id = "${escapeVrlString(entry.applicationId)}"`,
|
||||
);
|
||||
});
|
||||
lines.push(" }");
|
||||
lines.push("}");
|
||||
return lines.join("\n");
|
||||
};
|
||||
const buildScopeTransformSource = (): string =>
|
||||
[
|
||||
'.dokploy_organization = .label."dokploy.organization.id" || ""',
|
||||
'.dokploy_project = .label."dokploy.project" || ""',
|
||||
'.dokploy_project_id = .label."dokploy.project.id" || ""',
|
||||
'.dokploy_environment = .label."dokploy.environment" || ""',
|
||||
'.dokploy_environment_id = .label."dokploy.environment.id" || ""',
|
||||
'.dokploy_application = .label."dokploy.application" || ""',
|
||||
'.dokploy_application_id = .label."dokploy.application.id" || ""',
|
||||
'.dokploy_service = .label."dokploy.service" || ""',
|
||||
].join("\n");
|
||||
|
||||
const buildSinksAndTransforms = (
|
||||
providers: Array<LogProvider>,
|
||||
@@ -206,28 +90,22 @@ const buildSinksAndTransforms = (
|
||||
return { sinks, transforms };
|
||||
};
|
||||
|
||||
export interface VectorOrgData {
|
||||
providers: Array<LogProvider>;
|
||||
lookup: Record<string, AppNameLookupEntry>;
|
||||
}
|
||||
|
||||
export const loadVectorOrgData = async (
|
||||
const loadEnabledLogProviders = async (
|
||||
organizationId: string,
|
||||
): Promise<VectorOrgData> => {
|
||||
const [providers, lookup] = await Promise.all([
|
||||
findEnabledLogProvidersByOrganization(organizationId),
|
||||
buildAppNameLookup(organizationId),
|
||||
]);
|
||||
return { providers, lookup };
|
||||
logProviderIds?: string[],
|
||||
): Promise<Array<LogProvider>> => {
|
||||
const providers = await findEnabledLogProvidersByOrganization(organizationId);
|
||||
return logProviderIds
|
||||
? providers.filter((provider) =>
|
||||
logProviderIds.includes(provider.logProviderId),
|
||||
)
|
||||
: providers;
|
||||
};
|
||||
|
||||
export const buildVectorConfigYaml = async (
|
||||
organizationId: string,
|
||||
preloaded?: VectorOrgData,
|
||||
providers: Array<LogProvider>,
|
||||
options?: { dropUnmatched?: boolean },
|
||||
) => {
|
||||
const { providers, lookup } =
|
||||
preloaded ?? (await loadVectorOrgData(organizationId));
|
||||
const baseTransformId = options?.dropUnmatched
|
||||
? "dokploy_scope_local_only"
|
||||
: "dokploy_scope";
|
||||
@@ -248,7 +126,7 @@ export const buildVectorConfigYaml = async (
|
||||
dokploy_scope: {
|
||||
type: "remap",
|
||||
inputs: ["docker_logs_source"],
|
||||
source: buildScopeTransformSource(organizationId, lookup),
|
||||
source: buildScopeTransformSource(),
|
||||
},
|
||||
...(options?.dropUnmatched
|
||||
? {
|
||||
@@ -290,138 +168,89 @@ export const redactSecrets = (text: string, secrets: string[]): string => {
|
||||
return redacted;
|
||||
};
|
||||
|
||||
const VECTOR_CONFIG_LOCK_NAMESPACE = 87_234_501;
|
||||
|
||||
const hashServerIdToInt32 = (serverId: string): number => {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < serverId.length; i++) {
|
||||
hash = (Math.imul(31, hash) + serverId.charCodeAt(i)) | 0;
|
||||
}
|
||||
return hash;
|
||||
};
|
||||
|
||||
let lockPool: ReturnType<typeof postgres> | undefined;
|
||||
const getLockPool = () => {
|
||||
if (!lockPool) {
|
||||
lockPool = postgres(dbUrl, { max: 10 });
|
||||
}
|
||||
return lockPool;
|
||||
};
|
||||
|
||||
const LOCK_RETRY_ATTEMPTS = 3;
|
||||
const LOCK_RETRY_DELAY_MS = 1000;
|
||||
|
||||
const configWriteLocks = new Map<string, Promise<void>>();
|
||||
|
||||
export const withConfigWriteLock = async <T>(
|
||||
serverId: string,
|
||||
fn: () => Promise<T>,
|
||||
): Promise<T> => {
|
||||
const previous = configWriteLocks.get(serverId) ?? Promise.resolve();
|
||||
let release: () => void = () => {};
|
||||
const current = new Promise<void>((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
configWriteLocks.set(
|
||||
serverId,
|
||||
previous.then(() => current),
|
||||
);
|
||||
await previous;
|
||||
try {
|
||||
return await runWithCrossProcessLock(serverId, fn);
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
};
|
||||
|
||||
const runWithCrossProcessLock = async <T>(
|
||||
serverId: string,
|
||||
fn: () => Promise<T>,
|
||||
): Promise<T> => {
|
||||
const key1 = VECTOR_CONFIG_LOCK_NAMESPACE;
|
||||
const key2 = hashServerIdToInt32(serverId);
|
||||
for (let attempt = 1; attempt <= LOCK_RETRY_ATTEMPTS; attempt++) {
|
||||
const reserved = await getLockPool().reserve();
|
||||
try {
|
||||
const rows =
|
||||
await reserved`select pg_try_advisory_lock(${key1}, ${key2}) as locked`;
|
||||
if (rows[0]?.locked) {
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
await reserved`select pg_advisory_unlock(${key1}, ${key2})`;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
reserved.release();
|
||||
}
|
||||
if (attempt < LOCK_RETRY_ATTEMPTS) {
|
||||
await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_DELAY_MS));
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
`Vector config sync for server ${serverId} is already in progress on another Dokploy instance, skipped`,
|
||||
);
|
||||
};
|
||||
|
||||
export const syncVectorConfig = async ({
|
||||
serverId,
|
||||
preloaded,
|
||||
}: {
|
||||
serverId: string;
|
||||
preloaded?: VectorOrgData;
|
||||
}) =>
|
||||
withConfigWriteLock(serverId, async () => {
|
||||
const getServerLogProviderIds = async (
|
||||
serverId?: string,
|
||||
): Promise<string[]> => {
|
||||
if (serverId) {
|
||||
const server = await findServerById(serverId);
|
||||
const { VECTOR_PATH } = paths(true);
|
||||
const configPath = `${VECTOR_PATH}/vector.yaml`;
|
||||
const candidatePath = `${VECTOR_PATH}/vector.yaml.candidate`;
|
||||
const orgData =
|
||||
preloaded ?? (await loadVectorOrgData(server.organizationId));
|
||||
const yamlStr = await buildVectorConfigYaml(server.organizationId, orgData);
|
||||
return server.logProviderIds ?? [];
|
||||
}
|
||||
const settings = await getWebServerSettings();
|
||||
return settings?.logProviderIds ?? [];
|
||||
};
|
||||
|
||||
await execAsyncRemote(serverId, `mkdir -p ${VECTOR_PATH}/data`);
|
||||
await writeFileRemote(serverId, candidatePath, yamlStr);
|
||||
await execAsyncRemote(
|
||||
serverId,
|
||||
`chmod 600 ${candidatePath} && chmod 700 ${VECTOR_PATH} && chmod 700 ${VECTOR_PATH}/data`,
|
||||
);
|
||||
try {
|
||||
await execAsyncRemote(
|
||||
serverId,
|
||||
`docker run --rm -v /var/run/docker.sock:/var/run/docker.sock:ro -v ${candidatePath}:/etc/vector/vector.yaml:ro ${VECTOR_IMAGE} validate --skip-healthchecks /etc/vector/vector.yaml`,
|
||||
);
|
||||
} catch (error) {
|
||||
const detail =
|
||||
error instanceof ExecError
|
||||
? error.stderr || error.stdout || error.message
|
||||
: error instanceof Error
|
||||
? error.message
|
||||
: String(error);
|
||||
const sanitizedDetail = redactSecrets(
|
||||
detail,
|
||||
collectSecretValues(orgData.providers),
|
||||
);
|
||||
await execAsyncRemote(serverId, `rm -f ${candidatePath}`).catch(
|
||||
(cleanupError) => {
|
||||
console.error(
|
||||
`[Vector] Failed to remove invalid config candidate ${candidatePath} on server ${serverId}:`,
|
||||
cleanupError,
|
||||
);
|
||||
},
|
||||
);
|
||||
throw new Error(
|
||||
`Generated Vector config failed validation, not applying it: ${sanitizedDetail}`,
|
||||
);
|
||||
}
|
||||
await execAsyncRemote(serverId, `mv ${candidatePath} ${configPath}`);
|
||||
const syncVectorConfig = async (
|
||||
organizationId: string,
|
||||
serverId?: string,
|
||||
logProviderIds?: string[],
|
||||
) => {
|
||||
const { VECTOR_PATH } = paths(!!serverId);
|
||||
const configPath = `${VECTOR_PATH}/vector.yaml`;
|
||||
const candidatePath = `${VECTOR_PATH}/vector.yaml.candidate.${nanoid(10)}`;
|
||||
const ids = logProviderIds ?? (await getServerLogProviderIds(serverId));
|
||||
const providers = await loadEnabledLogProviders(organizationId, ids);
|
||||
const yamlStr = await buildVectorConfigYaml(providers, {
|
||||
dropUnmatched: !serverId,
|
||||
});
|
||||
|
||||
export const buildServiceSettings = (
|
||||
vectorPath: string,
|
||||
): CreateServiceOptions => {
|
||||
const VECTOR_PATH = vectorPath;
|
||||
return {
|
||||
const writeCommand = `umask 077 && mkdir -p ${quote([`${VECTOR_PATH}/data`])} && chmod 700 ${quote([VECTOR_PATH])} ${quote([`${VECTOR_PATH}/data`])} && echo "${encodeBase64(yamlStr)}" | base64 -d > ${quote([candidatePath])}`;
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, writeCommand);
|
||||
} else {
|
||||
await execAsync(writeCommand);
|
||||
}
|
||||
|
||||
const validateCommand = `docker run --rm -v /var/run/docker.sock:/var/run/docker.sock:ro -v ${quote([`${candidatePath}:/etc/vector/vector.yaml:ro`])} ${VECTOR_IMAGE} validate --skip-healthchecks /etc/vector/vector.yaml`;
|
||||
try {
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, validateCommand);
|
||||
} else {
|
||||
await execAsync(validateCommand);
|
||||
}
|
||||
} catch (error) {
|
||||
const detail =
|
||||
error instanceof ExecError
|
||||
? error.stderr || error.stdout || error.message
|
||||
: error instanceof Error
|
||||
? error.message
|
||||
: String(error);
|
||||
const sanitizedDetail = redactSecrets(
|
||||
detail,
|
||||
collectSecretValues(providers),
|
||||
);
|
||||
const cleanupCommand = `rm -f ${quote([candidatePath])}`;
|
||||
try {
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, cleanupCommand);
|
||||
} else {
|
||||
await execAsync(cleanupCommand);
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
console.error(
|
||||
`[Vector] Failed to remove invalid config candidate ${candidatePath}${
|
||||
serverId ? ` on server ${serverId}` : ""
|
||||
}:`,
|
||||
cleanupError,
|
||||
);
|
||||
}
|
||||
throw new Error(
|
||||
`Generated Vector config failed validation, not applying it: ${sanitizedDetail}`,
|
||||
);
|
||||
}
|
||||
|
||||
const applyCommand = `mv ${quote([candidatePath])} ${quote([configPath])}`;
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, applyCommand);
|
||||
} else {
|
||||
await execAsync(applyCommand);
|
||||
}
|
||||
};
|
||||
|
||||
const deployVectorService = async (serverId?: string) => {
|
||||
const { VECTOR_PATH } = paths(!!serverId);
|
||||
const docker = await getRemoteDocker(serverId);
|
||||
|
||||
const settings: CreateServiceOptions = {
|
||||
Name: VECTOR_SERVICE_NAME,
|
||||
TaskTemplate: {
|
||||
ContainerSpec: {
|
||||
@@ -456,40 +285,10 @@ export const buildServiceSettings = (
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const stripImageDigest = (image: string): string =>
|
||||
image.split("@")[0] ?? image;
|
||||
|
||||
export const vectorServiceSpecUnchanged = (
|
||||
inspect: { Spec: { TaskTemplate: any; Mode?: unknown } },
|
||||
settings: CreateServiceOptions,
|
||||
): boolean => {
|
||||
const desiredTaskTemplate = settings.TaskTemplate as any;
|
||||
const existing = inspect.Spec.TaskTemplate?.ContainerSpec ?? {};
|
||||
const desired = desiredTaskTemplate?.ContainerSpec ?? {};
|
||||
return (
|
||||
stripImageDigest(existing.Image ?? "") ===
|
||||
stripImageDigest(desired.Image ?? "") &&
|
||||
JSON.stringify(existing.Args ?? []) ===
|
||||
JSON.stringify(desired.Args ?? []) &&
|
||||
JSON.stringify(existing.Mounts ?? []) ===
|
||||
JSON.stringify(desired.Mounts ?? []) &&
|
||||
JSON.stringify(inspect.Spec.Mode ?? {}) ===
|
||||
JSON.stringify(settings.Mode ?? {})
|
||||
);
|
||||
};
|
||||
|
||||
const deployVectorService = async (
|
||||
docker: Awaited<ReturnType<typeof getRemoteDocker>>,
|
||||
settings: CreateServiceOptions,
|
||||
) => {
|
||||
try {
|
||||
const service = docker.getService(VECTOR_SERVICE_NAME);
|
||||
const inspect = await service.inspect();
|
||||
if (vectorServiceSpecUnchanged(inspect, settings)) {
|
||||
return;
|
||||
}
|
||||
await service.update({
|
||||
version: Number.parseInt(inspect.Version.Index, 10),
|
||||
...settings,
|
||||
@@ -498,130 +297,31 @@ const deployVectorService = async (
|
||||
ForceUpdate: (inspect.Spec.TaskTemplate.ForceUpdate ?? 0) + 1,
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
if (error?.statusCode !== 404) {
|
||||
throw error;
|
||||
}
|
||||
await docker.createService(settings);
|
||||
}
|
||||
};
|
||||
|
||||
export const setupVectorAgent = async ({
|
||||
serverId,
|
||||
preloaded,
|
||||
}: {
|
||||
serverId: string;
|
||||
preloaded?: VectorOrgData;
|
||||
}) => {
|
||||
await syncVectorConfig({ serverId, preloaded });
|
||||
const { VECTOR_PATH } = paths(true);
|
||||
|
||||
await pullRemoteImage(VECTOR_IMAGE, serverId);
|
||||
const docker = await getRemoteDocker(serverId);
|
||||
await deployVectorService(docker, buildServiceSettings(VECTOR_PATH));
|
||||
};
|
||||
|
||||
export const removeVectorAgent = async ({ serverId }: { serverId: string }) => {
|
||||
const server = await findServerById(serverId);
|
||||
if (!server.sshKeyId) {
|
||||
throw new Error(`No SSH key configured for server ${serverId}`);
|
||||
}
|
||||
const docker = await getRemoteDocker(serverId);
|
||||
try {
|
||||
await docker.getService(VECTOR_SERVICE_NAME).remove();
|
||||
} catch (error: any) {
|
||||
if (error?.statusCode !== 404) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
const { VECTOR_PATH } = paths(true);
|
||||
await execAsyncRemote(serverId, `rm -rf ${VECTOR_PATH}`);
|
||||
};
|
||||
|
||||
export const syncVectorAgent = async ({
|
||||
serverId,
|
||||
preloaded,
|
||||
}: {
|
||||
serverId: string;
|
||||
preloaded?: VectorOrgData;
|
||||
}): Promise<{ installed: boolean }> => {
|
||||
const server = await findServerById(serverId);
|
||||
const hasProvider = preloaded
|
||||
? preloaded.providers.length > 0
|
||||
: await hasEnabledLogProvider(server.organizationId);
|
||||
const shouldRun = server.enableLogManagement && hasProvider;
|
||||
|
||||
if (shouldRun) {
|
||||
await setupVectorAgent({ serverId, preloaded });
|
||||
} else {
|
||||
await removeVectorAgent({ serverId });
|
||||
}
|
||||
|
||||
return { installed: shouldRun };
|
||||
};
|
||||
|
||||
export const syncWebVectorConfig = async (
|
||||
organizationId: string,
|
||||
preloaded?: VectorOrgData,
|
||||
) =>
|
||||
withConfigWriteLock("web", async () => {
|
||||
const { VECTOR_PATH } = paths();
|
||||
const configPath = `${VECTOR_PATH}/vector.yaml`;
|
||||
const candidatePath = `${VECTOR_PATH}/vector.yaml.candidate`;
|
||||
const orgData = preloaded ?? (await loadVectorOrgData(organizationId));
|
||||
const yamlStr = await buildVectorConfigYaml(organizationId, orgData, {
|
||||
dropUnmatched: true,
|
||||
});
|
||||
|
||||
mkdirSync(`${VECTOR_PATH}/data`, { recursive: true });
|
||||
writeFileSync(candidatePath, yamlStr, "utf8");
|
||||
fsChmodSync(candidatePath, 0o600);
|
||||
fsChmodSync(VECTOR_PATH, 0o700);
|
||||
fsChmodSync(`${VECTOR_PATH}/data`, 0o700);
|
||||
console.log("Vector Updated ✅");
|
||||
} catch (_) {
|
||||
try {
|
||||
await execAsync(
|
||||
`docker run --rm -v /var/run/docker.sock:/var/run/docker.sock:ro -v ${candidatePath}:/etc/vector/vector.yaml:ro ${VECTOR_IMAGE} validate --skip-healthchecks /etc/vector/vector.yaml`,
|
||||
);
|
||||
} catch (error) {
|
||||
const detail =
|
||||
error instanceof ExecError
|
||||
? error.stderr || error.stdout || error.message
|
||||
: error instanceof Error
|
||||
? error.message
|
||||
: String(error);
|
||||
const sanitizedDetail = redactSecrets(
|
||||
detail,
|
||||
collectSecretValues(orgData.providers),
|
||||
);
|
||||
try {
|
||||
rmSync(candidatePath, { force: true });
|
||||
} catch (cleanupError) {
|
||||
console.error(
|
||||
`[Vector] Failed to remove invalid local config candidate ${candidatePath}:`,
|
||||
cleanupError,
|
||||
);
|
||||
await docker.createService(settings);
|
||||
console.log("Vector Started ✅");
|
||||
} catch (error: any) {
|
||||
if (error?.statusCode !== 409) {
|
||||
throw error;
|
||||
}
|
||||
throw new Error(
|
||||
`Generated Vector config failed validation, not applying it: ${sanitizedDetail}`,
|
||||
);
|
||||
console.log("Vector service already exists, continuing...");
|
||||
}
|
||||
renameSync(candidatePath, configPath);
|
||||
});
|
||||
|
||||
export const setupWebVectorAgent = async (
|
||||
organizationId: string,
|
||||
preloaded?: VectorOrgData,
|
||||
) => {
|
||||
await syncWebVectorConfig(organizationId, preloaded);
|
||||
const { VECTOR_PATH } = paths();
|
||||
|
||||
await pullImage(VECTOR_IMAGE);
|
||||
const docker = await getRemoteDocker();
|
||||
await deployVectorService(docker, buildServiceSettings(VECTOR_PATH));
|
||||
}
|
||||
};
|
||||
|
||||
export const removeWebVectorAgent = async () => {
|
||||
const docker = await getRemoteDocker();
|
||||
export const setupVectorAgent = async (
|
||||
organizationId: string,
|
||||
serverId?: string,
|
||||
logProviderIds?: string[],
|
||||
) => {
|
||||
await syncVectorConfig(organizationId, serverId, logProviderIds);
|
||||
await deployVectorService(serverId);
|
||||
};
|
||||
|
||||
export const removeVectorAgent = async (serverId?: string) => {
|
||||
const docker = await getRemoteDocker(serverId);
|
||||
try {
|
||||
await docker.getService(VECTOR_SERVICE_NAME).remove();
|
||||
} catch (error: any) {
|
||||
@@ -629,28 +329,82 @@ export const removeWebVectorAgent = async () => {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
const { VECTOR_PATH } = paths();
|
||||
rmSync(VECTOR_PATH, { recursive: true, force: true });
|
||||
};
|
||||
|
||||
export const syncWebVectorAgent = async (
|
||||
preloaded?: VectorOrgData,
|
||||
): Promise<{ installed: boolean }> => {
|
||||
const settings = await getWebServerSettings();
|
||||
const organizationId = settings?.logManagementOrganizationId ?? null;
|
||||
const hasProvider = organizationId
|
||||
? preloaded
|
||||
? preloaded.providers.length > 0
|
||||
: await hasEnabledLogProvider(organizationId)
|
||||
: false;
|
||||
const shouldRun =
|
||||
!!settings?.enableLogManagement && !!organizationId && hasProvider;
|
||||
|
||||
if (shouldRun && organizationId) {
|
||||
await setupWebVectorAgent(organizationId, preloaded);
|
||||
const { VECTOR_PATH } = paths(!!serverId);
|
||||
const cleanupCommand = `rm -rf ${quote([VECTOR_PATH])}`;
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, cleanupCommand);
|
||||
} else {
|
||||
await removeWebVectorAgent();
|
||||
await execAsync(cleanupCommand);
|
||||
}
|
||||
|
||||
return { installed: shouldRun };
|
||||
};
|
||||
|
||||
const isVectorRunning = async (serverId?: string): Promise<boolean> => {
|
||||
const docker = await getRemoteDocker(serverId);
|
||||
try {
|
||||
await docker.getService(VECTOR_SERVICE_NAME).inspect();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const VECTOR_CHECK_TIMEOUT_MS = 5000;
|
||||
|
||||
const vectorStatus = async (
|
||||
serverId: string | null,
|
||||
): Promise<"running" | "stopped" | "unknown"> => {
|
||||
try {
|
||||
const running = await Promise.race([
|
||||
isVectorRunning(serverId ?? undefined),
|
||||
new Promise<never>((_, reject) =>
|
||||
setTimeout(() => reject(new Error("timeout")), VECTOR_CHECK_TIMEOUT_MS),
|
||||
),
|
||||
]);
|
||||
return running ? "running" : "stopped";
|
||||
} catch {
|
||||
return "unknown";
|
||||
}
|
||||
};
|
||||
|
||||
export const getLogManagementServerStatus = async (organizationId: string) => {
|
||||
const [servers, settings] = await Promise.all([
|
||||
findServersByOrganizationForLogManagement(organizationId),
|
||||
IS_CLOUD ? Promise.resolve(null) : getWebServerSettings(),
|
||||
]);
|
||||
|
||||
const webOwnsThisOrg =
|
||||
!!settings &&
|
||||
(settings.logManagementOrganizationId === null ||
|
||||
settings.logManagementOrganizationId === organizationId);
|
||||
|
||||
const targets: Array<{
|
||||
serverId: string | null;
|
||||
name: string;
|
||||
ipAddress: string | null;
|
||||
logProviderIds: string[];
|
||||
}> = [
|
||||
...(settings && webOwnsThisOrg
|
||||
? [
|
||||
{
|
||||
serverId: null,
|
||||
name: "Dokploy Server (local)",
|
||||
ipAddress: null,
|
||||
logProviderIds: settings.logProviderIds ?? [],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...servers.map((s) => ({
|
||||
serverId: s.serverId,
|
||||
name: s.name,
|
||||
ipAddress: s.ipAddress,
|
||||
logProviderIds: s.logProviderIds ?? [],
|
||||
})),
|
||||
];
|
||||
|
||||
return Promise.all(
|
||||
targets.map(async (target) => ({
|
||||
...target,
|
||||
status: await vectorStatus(target.serverId),
|
||||
})),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import { findRegistryByIdWithCredentials } from "@dokploy/server/services/regist
|
||||
import type { InferResultType } from "@dokploy/server/types/with";
|
||||
import type { CreateServiceOptions } from "dockerode";
|
||||
import { getRegistryTag, uploadImageRemoteCommand } from "../cluster/upload";
|
||||
import { buildDokployLabels } from "../docker/dokploy-labels";
|
||||
import {
|
||||
calculateResources,
|
||||
generateBindMounts,
|
||||
@@ -150,7 +151,18 @@ export const mechanizeDockerContainer = async (
|
||||
Args: args,
|
||||
}),
|
||||
...(Ulimits && { Ulimits }),
|
||||
Labels,
|
||||
Labels: {
|
||||
...Labels,
|
||||
...buildDokployLabels({
|
||||
organizationId: application.environment.project.organizationId,
|
||||
projectId: application.environment.projectId,
|
||||
projectName: application.environment.project.name,
|
||||
environmentId: application.environmentId,
|
||||
environmentName: application.environment.name,
|
||||
applicationId: application.applicationId,
|
||||
applicationName: application.name,
|
||||
}),
|
||||
},
|
||||
},
|
||||
Networks: resolvedNetworks,
|
||||
RestartPolicy,
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
export interface DokployResourceMetadata {
|
||||
organizationId: string;
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
environmentId: string;
|
||||
environmentName: string;
|
||||
applicationId: string;
|
||||
applicationName: string;
|
||||
serviceName?: string;
|
||||
}
|
||||
|
||||
export const DOKPLOY_LABEL_PREFIX = "dokploy";
|
||||
|
||||
export const buildDokployLabels = (
|
||||
metadata: DokployResourceMetadata,
|
||||
): Record<string, string> => ({
|
||||
[`${DOKPLOY_LABEL_PREFIX}.organization.id`]: metadata.organizationId,
|
||||
[`${DOKPLOY_LABEL_PREFIX}.project.id`]: metadata.projectId,
|
||||
[`${DOKPLOY_LABEL_PREFIX}.project`]: metadata.projectName,
|
||||
[`${DOKPLOY_LABEL_PREFIX}.environment.id`]: metadata.environmentId,
|
||||
[`${DOKPLOY_LABEL_PREFIX}.environment`]: metadata.environmentName,
|
||||
[`${DOKPLOY_LABEL_PREFIX}.application.id`]: metadata.applicationId,
|
||||
[`${DOKPLOY_LABEL_PREFIX}.application`]: metadata.applicationName,
|
||||
...(metadata.serviceName
|
||||
? { [`${DOKPLOY_LABEL_PREFIX}.service`]: metadata.serviceName }
|
||||
: {}),
|
||||
});
|
||||
|
||||
export const dokployLabelsAsList = (
|
||||
metadata: DokployResourceMetadata,
|
||||
): string[] =>
|
||||
Object.entries(buildDokployLabels(metadata)).map(
|
||||
([key, value]) => `${key}=${value}`,
|
||||
);
|
||||
@@ -17,6 +17,7 @@ import { cloneGitlabRepository } from "../providers/gitlab";
|
||||
import { getCreateComposeFileCommand } from "../providers/raw";
|
||||
import { randomizeDeployableSpecificationFile } from "./collision";
|
||||
import { randomizeSpecificationFile } from "./compose";
|
||||
import { buildDokployLabels } from "./dokploy-labels";
|
||||
import type {
|
||||
ComposeSpecification,
|
||||
DefinitionsService,
|
||||
@@ -111,7 +112,7 @@ export const readComposeFile = async (compose: Compose) => {
|
||||
};
|
||||
|
||||
export const writeDomainsToCompose = async (
|
||||
compose: Compose,
|
||||
compose: ComposeWithMetadata,
|
||||
domains: Domain[],
|
||||
) => {
|
||||
try {
|
||||
@@ -196,8 +197,66 @@ const removeDomainLabels = (
|
||||
return labels;
|
||||
};
|
||||
|
||||
type ComposeWithMetadata = Compose & {
|
||||
environment?: {
|
||||
environmentId: string;
|
||||
name: string;
|
||||
projectId: string;
|
||||
project?: { name: string; organizationId: string };
|
||||
};
|
||||
};
|
||||
|
||||
const applyDokployLabelsToCompose = (
|
||||
result: ComposeSpecification,
|
||||
compose: ComposeWithMetadata,
|
||||
) => {
|
||||
const { environment } = compose;
|
||||
if (!environment?.project) return;
|
||||
|
||||
for (const [serviceName, service] of Object.entries(result.services ?? {})) {
|
||||
if (!service) continue;
|
||||
const dokployLabels = buildDokployLabels({
|
||||
organizationId: environment.project.organizationId,
|
||||
projectId: environment.projectId,
|
||||
projectName: environment.project.name,
|
||||
environmentId: environment.environmentId,
|
||||
environmentName: environment.name,
|
||||
applicationId: compose.composeId,
|
||||
applicationName: compose.name,
|
||||
serviceName,
|
||||
});
|
||||
|
||||
if (compose.composeType === "docker-compose") {
|
||||
service.labels = mergeDokployLabels(service.labels, dokployLabels);
|
||||
continue;
|
||||
}
|
||||
if (!service.deploy) {
|
||||
service.deploy = {};
|
||||
}
|
||||
service.deploy.labels = mergeDokployLabels(
|
||||
service.deploy.labels,
|
||||
dokployLabels,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const mergeDokployLabels = (
|
||||
current: DefinitionsService["labels"],
|
||||
dokployLabels: Record<string, string>,
|
||||
): DefinitionsService["labels"] => {
|
||||
if (current && !Array.isArray(current)) {
|
||||
return { ...current, ...dokployLabels };
|
||||
}
|
||||
const entries = Object.entries(dokployLabels);
|
||||
const keys = new Set(entries.map(([key]) => key));
|
||||
const kept = (current ?? []).filter(
|
||||
(label) => !keys.has(label.slice(0, label.indexOf("="))),
|
||||
);
|
||||
return [...kept, ...entries.map(([key, value]) => `${key}=${value}`)];
|
||||
};
|
||||
|
||||
export const addDomainToCompose = async (
|
||||
compose: Compose,
|
||||
compose: ComposeWithMetadata,
|
||||
domains: Domain[],
|
||||
) => {
|
||||
const { appName } = compose;
|
||||
@@ -326,6 +385,8 @@ export const addDomainToCompose = async (
|
||||
}
|
||||
}
|
||||
|
||||
applyDokployLabelsToCompose(result, compose);
|
||||
|
||||
const injectedNetworkNames = await applyServiceNetworks(result, compose);
|
||||
|
||||
if (!compose.isolatedDeployment) {
|
||||
|
||||
Reference in New Issue
Block a user