mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-10-19 07:01:35 +00:00
fix(log-management): apply Greptile review fixes for agent lifecycle
- Check accessedServers on deploy, removal and the server-status list - Keep the local claim until the agent is removed and serialize deploy/remove per target - Persist provider assignments only after the Vector deploy succeeds; release the local claim if the first deploy fails - Remove the agent on server/organization deletion even when no providers remain assigned - Report agents with no running task as not running - Refresh logProvider.one after saving, allow clearing saved credentials, keep deployed targets visible with no providers
This commit is contained in:
@@ -262,6 +262,41 @@ describe("updateLogProvider — merges partial update with existing row before v
|
||||
);
|
||||
});
|
||||
|
||||
it("clears an optional credential when it is explicitly set to null", async () => {
|
||||
mocks.findFirst.mockResolvedValue({
|
||||
logProviderId: "lp-1",
|
||||
name: "loki-prod",
|
||||
providerType: "loki",
|
||||
endpoint: "https://loki.example.com",
|
||||
apiKey: "old-token",
|
||||
apiSecret: null,
|
||||
extraConfig: { username: "user" },
|
||||
});
|
||||
mocks.updateSet.mockReturnValue([{ logProviderId: "lp-1" }]);
|
||||
|
||||
await updateLogProvider("lp-1", { apiKey: null });
|
||||
|
||||
expect(mocks.updateSet).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ apiKey: null }),
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects clearing the elasticsearch password while a username stays saved", async () => {
|
||||
mocks.findFirst.mockResolvedValue({
|
||||
logProviderId: "lp-1",
|
||||
name: "es-prod",
|
||||
providerType: "elasticsearch",
|
||||
endpoint: "https://es.example.com:9200",
|
||||
apiKey: "old-password",
|
||||
apiSecret: null,
|
||||
extraConfig: { username: "elastic" },
|
||||
});
|
||||
|
||||
await expect(updateLogProvider("lp-1", { apiKey: null })).rejects.toThrow(
|
||||
/Password/,
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects a providerType change that doesn't satisfy the new type's required fields", async () => {
|
||||
mocks.findFirst.mockResolvedValue({
|
||||
logProviderId: "lp-1",
|
||||
|
||||
@@ -55,8 +55,8 @@ describe("removeLogProvider", () => {
|
||||
expect(render(mocks.updates[1]?.values.logProviderIds)).toBe(
|
||||
'array_remove("webServerSettings"."logProviderIds", $1)',
|
||||
);
|
||||
expect(render(mocks.updates[1]?.values.logManagementOrganizationId)).toBe(
|
||||
'CASE WHEN cardinality(array_remove("webServerSettings"."logProviderIds", $1)) = 0 THEN NULL ELSE "webServerSettings"."logManagementOrganizationId" END',
|
||||
expect(mocks.updates[1]?.values).not.toHaveProperty(
|
||||
"logManagementOrganizationId",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,10 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
claimWebServerLogManagement: vi.fn(),
|
||||
releaseWebServerLogManagement: vi.fn(),
|
||||
getWebServerSettings: vi.fn(),
|
||||
updateServerLogProviders: vi.fn(),
|
||||
getAccessibleServerIds: vi.fn(),
|
||||
removeVectorAgent: vi.fn(),
|
||||
setupVectorAgent: vi.fn(),
|
||||
assertServerBelongsToOrg: vi.fn(),
|
||||
@@ -14,13 +17,18 @@ const loadAgent = async (isCloud: boolean) => {
|
||||
vi.doMock("@dokploy/server/constants", () => ({ IS_CLOUD: isCloud }));
|
||||
vi.doMock("@dokploy/server/services/server", () => ({
|
||||
updateServerLogProviders: mocks.updateServerLogProviders,
|
||||
getAccessibleServerIds: mocks.getAccessibleServerIds,
|
||||
}));
|
||||
vi.doMock("@dokploy/server/services/web-server-settings", () => ({
|
||||
claimWebServerLogManagement: mocks.claimWebServerLogManagement,
|
||||
releaseWebServerLogManagement: mocks.releaseWebServerLogManagement,
|
||||
getWebServerSettings: mocks.getWebServerSettings,
|
||||
}));
|
||||
vi.doMock("@dokploy/server/setup/vector-setup", () => ({
|
||||
removeVectorAgent: mocks.removeVectorAgent,
|
||||
setupVectorAgent: mocks.setupVectorAgent,
|
||||
withVectorTargetLock: (_: string | undefined, fn: () => Promise<unknown>) =>
|
||||
fn(),
|
||||
}));
|
||||
vi.doMock("@dokploy/server/services/log-management/service", () => ({
|
||||
assertServerBelongsToOrg: mocks.assertServerBelongsToOrg,
|
||||
@@ -29,27 +37,62 @@ const loadAgent = async (isCloud: boolean) => {
|
||||
return await import("@dokploy/server/services/log-management/vector-agent");
|
||||
};
|
||||
|
||||
const sessionA = { userId: "user-a", activeOrganizationId: "org-a" };
|
||||
const sessionB = { userId: "user-b", activeOrganizationId: "org-b" };
|
||||
|
||||
describe("local Vector agent guards", () => {
|
||||
beforeEach(() => {
|
||||
for (const mock of Object.values(mocks)) mock.mockReset();
|
||||
mocks.getAccessibleServerIds.mockResolvedValue(new Set(["server-1"]));
|
||||
});
|
||||
|
||||
it("does not remove the local agent when another organization owns it", async () => {
|
||||
mocks.claimWebServerLogManagement.mockResolvedValue(null);
|
||||
mocks.getWebServerSettings.mockResolvedValue({
|
||||
logManagementOrganizationId: "org-a",
|
||||
});
|
||||
const { removeLogManagement } = await loadAgent(false);
|
||||
|
||||
await expect(removeLogManagement("org-b", undefined)).rejects.toMatchObject(
|
||||
{ code: "CONFLICT" },
|
||||
);
|
||||
expect(mocks.claimWebServerLogManagement).toHaveBeenCalledWith("org-b", []);
|
||||
await expect(
|
||||
removeLogManagement(sessionB, undefined),
|
||||
).rejects.toMatchObject({ code: "CONFLICT" });
|
||||
expect(mocks.removeVectorAgent).not.toHaveBeenCalled();
|
||||
expect(mocks.releaseWebServerLogManagement).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("removes the local agent once the claim is released", async () => {
|
||||
mocks.claimWebServerLogManagement.mockResolvedValue({ id: "settings" });
|
||||
it("removes the local agent and only then releases the claim", async () => {
|
||||
mocks.getWebServerSettings.mockResolvedValue({
|
||||
logManagementOrganizationId: "org-a",
|
||||
});
|
||||
const { removeLogManagement } = await loadAgent(false);
|
||||
|
||||
await removeLogManagement("org-a", undefined);
|
||||
await removeLogManagement(sessionA, undefined);
|
||||
expect(mocks.removeVectorAgent).toHaveBeenCalledWith();
|
||||
expect(mocks.releaseWebServerLogManagement).toHaveBeenCalledWith("org-a");
|
||||
expect(mocks.removeVectorAgent.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
mocks.releaseWebServerLogManagement.mock.invocationCallOrder[0] ?? 0,
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps the claim when removing the local agent fails", async () => {
|
||||
mocks.getWebServerSettings.mockResolvedValue({
|
||||
logManagementOrganizationId: "org-a",
|
||||
});
|
||||
mocks.removeVectorAgent.mockRejectedValue(new Error("docker down"));
|
||||
const { removeLogManagement } = await loadAgent(false);
|
||||
|
||||
await expect(removeLogManagement(sessionA, undefined)).rejects.toThrow(
|
||||
"docker down",
|
||||
);
|
||||
expect(mocks.releaseWebServerLogManagement).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("removes an unclaimed local agent", async () => {
|
||||
mocks.getWebServerSettings.mockResolvedValue({
|
||||
logManagementOrganizationId: null,
|
||||
});
|
||||
const { removeLogManagement } = await loadAgent(false);
|
||||
|
||||
await removeLogManagement(sessionA, undefined);
|
||||
expect(mocks.removeVectorAgent).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
@@ -57,7 +100,7 @@ describe("local Vector agent guards", () => {
|
||||
const { deployLogManagement } = await loadAgent(true);
|
||||
|
||||
await expect(
|
||||
deployLogManagement("org-a", undefined, ["lp-1"]),
|
||||
deployLogManagement(sessionA, undefined, ["lp-1"]),
|
||||
).rejects.toMatchObject({ code: "BAD_REQUEST" });
|
||||
expect(mocks.claimWebServerLogManagement).not.toHaveBeenCalled();
|
||||
expect(mocks.setupVectorAgent).not.toHaveBeenCalled();
|
||||
@@ -66,17 +109,17 @@ describe("local Vector agent guards", () => {
|
||||
it("rejects removing the local agent in cloud", async () => {
|
||||
const { removeLogManagement } = await loadAgent(true);
|
||||
|
||||
await expect(removeLogManagement("org-a", undefined)).rejects.toMatchObject(
|
||||
{ code: "BAD_REQUEST" },
|
||||
);
|
||||
expect(mocks.claimWebServerLogManagement).not.toHaveBeenCalled();
|
||||
await expect(
|
||||
removeLogManagement(sessionA, undefined),
|
||||
).rejects.toMatchObject({ code: "BAD_REQUEST" });
|
||||
expect(mocks.getWebServerSettings).not.toHaveBeenCalled();
|
||||
expect(mocks.removeVectorAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("still manages remote servers in cloud", async () => {
|
||||
const { removeLogManagement } = await loadAgent(true);
|
||||
|
||||
await removeLogManagement("org-a", "server-1");
|
||||
await removeLogManagement(sessionA, "server-1");
|
||||
expect(mocks.assertServerBelongsToOrg).toHaveBeenCalledWith(
|
||||
"server-1",
|
||||
"org-a",
|
||||
@@ -85,3 +128,139 @@ describe("local Vector agent guards", () => {
|
||||
expect(mocks.updateServerLogProviders).toHaveBeenCalledWith("server-1", []);
|
||||
});
|
||||
});
|
||||
|
||||
describe("remote server access", () => {
|
||||
beforeEach(() => {
|
||||
for (const mock of Object.values(mocks)) mock.mockReset();
|
||||
mocks.getAccessibleServerIds.mockResolvedValue(new Set(["server-1"]));
|
||||
});
|
||||
|
||||
it("rejects deploying on a server the member cannot access", async () => {
|
||||
const { deployLogManagement } = await loadAgent(false);
|
||||
|
||||
await expect(
|
||||
deployLogManagement(sessionA, "server-2", ["lp-1"]),
|
||||
).rejects.toMatchObject({ code: "UNAUTHORIZED" });
|
||||
expect(mocks.getAccessibleServerIds).toHaveBeenCalledWith(sessionA);
|
||||
expect(mocks.updateServerLogProviders).not.toHaveBeenCalled();
|
||||
expect(mocks.setupVectorAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects removing from a server the member cannot access", async () => {
|
||||
const { removeLogManagement } = await loadAgent(false);
|
||||
|
||||
await expect(
|
||||
removeLogManagement(sessionA, "server-2"),
|
||||
).rejects.toMatchObject({ code: "UNAUTHORIZED" });
|
||||
expect(mocks.removeVectorAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("deploys on an accessible server", async () => {
|
||||
const { deployLogManagement } = await loadAgent(false);
|
||||
|
||||
await deployLogManagement(sessionA, "server-1", ["lp-1"]);
|
||||
expect(mocks.updateServerLogProviders).toHaveBeenCalledWith("server-1", [
|
||||
"lp-1",
|
||||
]);
|
||||
expect(mocks.setupVectorAgent).toHaveBeenCalledWith("org-a", "server-1", [
|
||||
"lp-1",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps the saved providers when the server deploy fails", async () => {
|
||||
mocks.setupVectorAgent.mockRejectedValue(new Error("validation failed"));
|
||||
const { deployLogManagement } = await loadAgent(false);
|
||||
|
||||
await expect(
|
||||
deployLogManagement(sessionA, "server-1", ["lp-2"]),
|
||||
).rejects.toThrow("validation failed");
|
||||
expect(mocks.updateServerLogProviders).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("removes the agent on server deletion even with no providers left", async () => {
|
||||
const { removeServerLogManagement } = await loadAgent(false);
|
||||
|
||||
await removeServerLogManagement({
|
||||
serverId: "server-1",
|
||||
sshKeyId: "key-1",
|
||||
logProviderIds: [],
|
||||
} as never);
|
||||
expect(mocks.removeVectorAgent).toHaveBeenCalledWith("server-1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("local agent deploy", () => {
|
||||
beforeEach(() => {
|
||||
for (const mock of Object.values(mocks)) mock.mockReset();
|
||||
mocks.claimWebServerLogManagement.mockResolvedValue({});
|
||||
});
|
||||
|
||||
it("saves the new providers only after the deploy succeeds", async () => {
|
||||
mocks.getWebServerSettings.mockResolvedValue({
|
||||
logManagementOrganizationId: "org-a",
|
||||
logProviderIds: ["lp-1"],
|
||||
});
|
||||
const { deployLogManagement } = await loadAgent(false);
|
||||
|
||||
await deployLogManagement(sessionA, undefined, ["lp-2"]);
|
||||
expect(mocks.claimWebServerLogManagement).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"org-a",
|
||||
["lp-1"],
|
||||
);
|
||||
expect(mocks.claimWebServerLogManagement).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
"org-a",
|
||||
["lp-2"],
|
||||
);
|
||||
expect(mocks.setupVectorAgent.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
mocks.claimWebServerLogManagement.mock.invocationCallOrder[1] ?? 0,
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps the previous providers when a redeploy fails", async () => {
|
||||
mocks.getWebServerSettings.mockResolvedValue({
|
||||
logManagementOrganizationId: "org-a",
|
||||
logProviderIds: ["lp-1"],
|
||||
});
|
||||
mocks.setupVectorAgent.mockRejectedValue(new Error("validation failed"));
|
||||
const { deployLogManagement } = await loadAgent(false);
|
||||
|
||||
await expect(
|
||||
deployLogManagement(sessionA, undefined, ["lp-2"]),
|
||||
).rejects.toThrow("validation failed");
|
||||
expect(mocks.claimWebServerLogManagement).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.claimWebServerLogManagement).toHaveBeenCalledWith("org-a", [
|
||||
"lp-1",
|
||||
]);
|
||||
expect(mocks.releaseWebServerLogManagement).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("releases the claim when the first local deploy fails", async () => {
|
||||
mocks.getWebServerSettings.mockResolvedValue({
|
||||
logManagementOrganizationId: null,
|
||||
logProviderIds: [],
|
||||
});
|
||||
mocks.setupVectorAgent.mockRejectedValue(new Error("validation failed"));
|
||||
const { deployLogManagement } = await loadAgent(false);
|
||||
|
||||
await expect(
|
||||
deployLogManagement(sessionA, undefined, ["lp-1"]),
|
||||
).rejects.toThrow("validation failed");
|
||||
expect(mocks.releaseWebServerLogManagement).toHaveBeenCalledWith("org-a");
|
||||
});
|
||||
|
||||
it("rejects deploying when another organization owns the local agent", async () => {
|
||||
mocks.getWebServerSettings.mockResolvedValue({
|
||||
logManagementOrganizationId: "org-a",
|
||||
logProviderIds: ["lp-1"],
|
||||
});
|
||||
mocks.claimWebServerLogManagement.mockResolvedValue(null);
|
||||
const { deployLogManagement } = await loadAgent(false);
|
||||
|
||||
await expect(
|
||||
deployLogManagement(sessionB, undefined, ["lp-9"]),
|
||||
).rejects.toMatchObject({ code: "CONFLICT" });
|
||||
expect(mocks.setupVectorAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
listTasks: vi.fn(),
|
||||
findServersByOrganizationForLogManagement: vi.fn(),
|
||||
getAccessibleServerIds: vi.fn(),
|
||||
getWebServerSettings: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@dokploy/server/db", () => ({ db: {} }));
|
||||
vi.mock("@dokploy/server/constants", () => ({
|
||||
IS_CLOUD: false,
|
||||
paths: () => ({ VECTOR_PATH: "/etc/dokploy/vector" }),
|
||||
}));
|
||||
vi.mock("@dokploy/server/services/server", () => ({
|
||||
findServerById: vi.fn(),
|
||||
findServersByOrganizationForLogManagement:
|
||||
mocks.findServersByOrganizationForLogManagement,
|
||||
getAccessibleServerIds: mocks.getAccessibleServerIds,
|
||||
}));
|
||||
vi.mock("@dokploy/server/services/web-server-settings", () => ({
|
||||
getWebServerSettings: mocks.getWebServerSettings,
|
||||
}));
|
||||
vi.mock("@dokploy/server/utils/servers/remote-docker", () => ({
|
||||
getRemoteDocker: async () => ({ listTasks: mocks.listTasks }),
|
||||
}));
|
||||
|
||||
const { getLogManagementServerStatus, withVectorTargetLock } = await import(
|
||||
"@dokploy/server/setup/vector-setup"
|
||||
);
|
||||
|
||||
const session = { userId: "user-1", activeOrganizationId: "org-1" };
|
||||
|
||||
describe("getLogManagementServerStatus", () => {
|
||||
beforeEach(() => {
|
||||
for (const mock of Object.values(mocks)) mock.mockReset();
|
||||
mocks.findServersByOrganizationForLogManagement.mockResolvedValue([]);
|
||||
mocks.getAccessibleServerIds.mockResolvedValue(new Set());
|
||||
mocks.getWebServerSettings.mockResolvedValue({
|
||||
logManagementOrganizationId: "org-1",
|
||||
logProviderIds: ["lp-1"],
|
||||
});
|
||||
});
|
||||
|
||||
it("reports running only when a task of the service is running", async () => {
|
||||
mocks.listTasks.mockResolvedValue([{ Status: { State: "running" } }]);
|
||||
|
||||
const [local] = await getLogManagementServerStatus(session);
|
||||
expect(local?.status).toBe("running");
|
||||
expect(mocks.listTasks).toHaveBeenCalledWith({
|
||||
filters: JSON.stringify({
|
||||
service: ["dokploy-vector"],
|
||||
"desired-state": ["running"],
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it("reports not-running when the service has no running task", async () => {
|
||||
mocks.listTasks.mockResolvedValue([{ Status: { State: "pending" } }]);
|
||||
|
||||
const [local] = await getLogManagementServerStatus(session);
|
||||
expect(local?.status).toBe("not-running");
|
||||
});
|
||||
|
||||
it("reports stopped when the service does not exist", async () => {
|
||||
mocks.listTasks.mockRejectedValue(
|
||||
Object.assign(new Error("not found"), { statusCode: 404 }),
|
||||
);
|
||||
|
||||
const [local] = await getLogManagementServerStatus(session);
|
||||
expect(local?.status).toBe("stopped");
|
||||
});
|
||||
|
||||
it("reports unknown on any other docker error", async () => {
|
||||
mocks.listTasks.mockRejectedValue(new Error("connect ECONNREFUSED"));
|
||||
|
||||
const [local] = await getLogManagementServerStatus(session);
|
||||
expect(local?.status).toBe("unknown");
|
||||
});
|
||||
|
||||
it("hides servers the member cannot access", async () => {
|
||||
mocks.listTasks.mockResolvedValue([]);
|
||||
mocks.findServersByOrganizationForLogManagement.mockResolvedValue([
|
||||
{ serverId: "server-1", name: "one", ipAddress: "1.1.1.1" },
|
||||
{ serverId: "server-2", name: "two", ipAddress: "2.2.2.2" },
|
||||
]);
|
||||
mocks.getAccessibleServerIds.mockResolvedValue(new Set(["server-2"]));
|
||||
|
||||
const targets = await getLogManagementServerStatus(session);
|
||||
expect(targets.map((t) => t.serverId)).toEqual([null, "server-2"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("withVectorTargetLock", () => {
|
||||
it("runs operations on the same target one after another", async () => {
|
||||
const events: string[] = [];
|
||||
let releaseFirst = () => {};
|
||||
const first = withVectorTargetLock("server-1", async () => {
|
||||
events.push("first:start");
|
||||
await new Promise<void>((resolve) => {
|
||||
releaseFirst = resolve;
|
||||
});
|
||||
events.push("first:end");
|
||||
throw new Error("first failed");
|
||||
});
|
||||
const second = withVectorTargetLock("server-1", async () => {
|
||||
events.push("second");
|
||||
});
|
||||
const other = withVectorTargetLock("server-2", async () => {
|
||||
events.push("other");
|
||||
});
|
||||
|
||||
await other;
|
||||
expect(events).toEqual(["first:start", "other"]);
|
||||
releaseFirst();
|
||||
await expect(first).rejects.toThrow("first failed");
|
||||
await second;
|
||||
expect(events).toEqual(["first:start", "other", "first:end", "second"]);
|
||||
});
|
||||
});
|
||||
@@ -50,6 +50,7 @@ export const HandleLogProvider = ({ logProviderId }: Props) => {
|
||||
const [initialFieldValues, setInitialFieldValues] = useState<
|
||||
Record<string, string>
|
||||
>({});
|
||||
const [clearedKeys, setClearedKeys] = useState<Record<string, boolean>>({});
|
||||
|
||||
const { data: availableTypes } = api.logProvider.availableTypes.useQuery();
|
||||
const { data: provider } = api.logProvider.one.useQuery(
|
||||
@@ -96,12 +97,14 @@ export const HandleLogProvider = ({ logProviderId }: Props) => {
|
||||
>;
|
||||
setFieldValues(extraConfig);
|
||||
setInitialFieldValues(extraConfig);
|
||||
setClearedKeys({});
|
||||
} else if (isOpen) {
|
||||
setName("");
|
||||
setProviderType("");
|
||||
setEnabled(true);
|
||||
setFieldValues({});
|
||||
setInitialFieldValues({});
|
||||
setClearedKeys({});
|
||||
}
|
||||
}, [provider, isOpen]);
|
||||
|
||||
@@ -114,6 +117,8 @@ export const HandleLogProvider = ({ logProviderId }: Props) => {
|
||||
if (TOP_LEVEL_KEYS.has(field.key)) {
|
||||
if (value) {
|
||||
payload[field.key] = value;
|
||||
} else if (clearedKeys[field.key]) {
|
||||
payload[field.key] = null;
|
||||
}
|
||||
} else {
|
||||
hasExtraConfigFields = true;
|
||||
@@ -129,7 +134,9 @@ export const HandleLogProvider = ({ logProviderId }: Props) => {
|
||||
const topLevelKeys = (selectedType?.credentialFields ?? [])
|
||||
.filter((field) => TOP_LEVEL_KEYS.has(field.key))
|
||||
.map((field) => field.key);
|
||||
const touchedTopLevelKeys = topLevelKeys.filter((key) => !!fieldValues[key]);
|
||||
const touchedTopLevelKeys = topLevelKeys.filter(
|
||||
(key) => !!fieldValues[key] || clearedKeys[key],
|
||||
);
|
||||
const extraConfigKeys = (selectedType?.credentialFields ?? [])
|
||||
.filter((field) => !TOP_LEVEL_KEYS.has(field.key))
|
||||
.map((field) => field.key);
|
||||
@@ -190,6 +197,9 @@ export const HandleLogProvider = ({ logProviderId }: Props) => {
|
||||
} as any)
|
||||
.then(() => {
|
||||
utils.logProvider.all.invalidate();
|
||||
if (logProviderId) {
|
||||
utils.logProvider.one.invalidate({ logProviderId });
|
||||
}
|
||||
const agents =
|
||||
logProviderId &&
|
||||
agentsUsingProvider(
|
||||
@@ -307,24 +317,49 @@ export const HandleLogProvider = ({ logProviderId }: Props) => {
|
||||
{field.helpText}
|
||||
</span>
|
||||
)}
|
||||
<Input
|
||||
type={field.type === "password" ? "password" : "text"}
|
||||
placeholder={
|
||||
logProviderId && TOP_LEVEL_KEYS.has(field.key)
|
||||
? "Leave blank to keep existing"
|
||||
: field.placeholder
|
||||
}
|
||||
autoComplete={
|
||||
field.type === "password" ? "one-time-code" : "off"
|
||||
}
|
||||
value={fieldValues[field.key] ?? ""}
|
||||
onChange={(e) =>
|
||||
setFieldValues((prev) => ({
|
||||
...prev,
|
||||
[field.key]: e.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<div className="flex flex-row gap-2">
|
||||
<Input
|
||||
type={field.type === "password" ? "password" : "text"}
|
||||
disabled={!!clearedKeys[field.key]}
|
||||
placeholder={
|
||||
clearedKeys[field.key]
|
||||
? "Will be cleared on update"
|
||||
: logProviderId && TOP_LEVEL_KEYS.has(field.key)
|
||||
? "Leave blank to keep existing"
|
||||
: field.placeholder
|
||||
}
|
||||
autoComplete={
|
||||
field.type === "password" ? "one-time-code" : "off"
|
||||
}
|
||||
value={fieldValues[field.key] ?? ""}
|
||||
onChange={(e) =>
|
||||
setFieldValues((prev) => ({
|
||||
...prev,
|
||||
[field.key]: e.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
{logProviderId &&
|
||||
TOP_LEVEL_KEYS.has(field.key) &&
|
||||
!field.required && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setClearedKeys((prev) => ({
|
||||
...prev,
|
||||
[field.key]: !prev[field.key],
|
||||
}));
|
||||
setFieldValues((prev) => ({
|
||||
...prev,
|
||||
[field.key]: "",
|
||||
}));
|
||||
}}
|
||||
>
|
||||
{clearedKeys[field.key] ? "Keep" : "Clear"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
|
||||
+37
-25
@@ -17,12 +17,15 @@ import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
type Status = "running" | "stopped" | "unknown";
|
||||
type Status = "running" | "not-running" | "stopped" | "unknown";
|
||||
|
||||
const statusBadge = (status: Status) => {
|
||||
if (status === "running") {
|
||||
return <Badge variant="green">Running</Badge>;
|
||||
}
|
||||
if (status === "not-running") {
|
||||
return <Badge variant="yellow">Not running</Badge>;
|
||||
}
|
||||
if (status === "stopped") {
|
||||
return <Badge variant="secondary">Not deployed</Badge>;
|
||||
}
|
||||
@@ -117,6 +120,9 @@ export const ShowLogManagementServers = () => {
|
||||
};
|
||||
|
||||
const hasProviders = (providers?.length ?? 0) > 0;
|
||||
const visibleTargets = hasProviders
|
||||
? targets
|
||||
: targets?.filter((target) => target.status !== "stopped");
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
@@ -130,7 +136,9 @@ export const ShowLogManagementServers = () => {
|
||||
<CardDescription>
|
||||
Pick which providers each server ships to, then deploy its Vector
|
||||
agent. Changing a provider only takes effect on a server after you
|
||||
redeploy it.
|
||||
redeploy it. Services deployed before Log Management need a
|
||||
redeploy to get the labels it uses; until then their logs are not
|
||||
shipped from the Dokploy server.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 py-8 border-t">
|
||||
@@ -139,7 +147,7 @@ export const ShowLogManagementServers = () => {
|
||||
<span>Checking servers...</span>
|
||||
<Loader2 className="animate-spin size-4" />
|
||||
</div>
|
||||
) : !hasProviders ? (
|
||||
) : !hasProviders && !visibleTargets?.length ? (
|
||||
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
|
||||
<ScrollText className="size-8 self-center text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground text-center">
|
||||
@@ -148,11 +156,11 @@ export const ShowLogManagementServers = () => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4">
|
||||
{targets?.map((target) => {
|
||||
{visibleTargets?.map((target) => {
|
||||
const key = keyOf(target.serverId);
|
||||
const ids = idsFor(target.serverId, target.logProviderIds);
|
||||
const isBusy = pendingTarget === key;
|
||||
const isDeployed = target.status === "running";
|
||||
const isDeployed = target.status !== "stopped";
|
||||
const hasEnabledSelected = ids.some(
|
||||
(id) =>
|
||||
providers?.find((p) => p.logProviderId === id)?.enabled,
|
||||
@@ -196,9 +204,11 @@ export const ShowLogManagementServers = () => {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-xs font-medium">
|
||||
Log providers
|
||||
</span>
|
||||
{hasProviders && (
|
||||
<span className="text-xs font-medium">
|
||||
Log providers
|
||||
</span>
|
||||
)}
|
||||
<div className="flex flex-col gap-2">
|
||||
{providers?.map((provider) => {
|
||||
const ProviderIcon =
|
||||
@@ -274,27 +284,29 @@ export const ShowLogManagementServers = () => {
|
||||
</AlertBlock>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
{ids.length > 0 && !hasEnabledSelected && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Every selected provider is disabled — nothing would
|
||||
ship.
|
||||
</span>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
isLoading={isBusy}
|
||||
disabled={!hasEnabledSelected}
|
||||
onClick={() => handleDeploy(target.serverId, ids)}
|
||||
>
|
||||
{isDeployed ? "Redeploy" : "Deploy"}
|
||||
</Button>
|
||||
</div>
|
||||
{hasProviders && (
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
{ids.length > 0 && !hasEnabledSelected && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Every selected provider is disabled — nothing
|
||||
would ship.
|
||||
</span>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
isLoading={isBusy}
|
||||
disabled={!hasEnabledSelected}
|
||||
onClick={() => handleDeploy(target.serverId, ids)}
|
||||
>
|
||||
{isDeployed ? "Redeploy" : "Deploy"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{targets?.length === 0 && (
|
||||
{visibleTargets?.length === 0 && (
|
||||
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
|
||||
<Server className="size-8 self-center text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground text-center">
|
||||
|
||||
@@ -111,7 +111,7 @@ export const logProviderRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
serverStatus: withPermission("logProvider", "read").query(({ ctx }) =>
|
||||
getLogManagementServerStatus(ctx.session.activeOrganizationId),
|
||||
getLogManagementServerStatus(ctx.session),
|
||||
),
|
||||
deployOnServer: withPermission("logProvider", "create")
|
||||
.input(
|
||||
@@ -122,7 +122,7 @@ export const logProviderRouter = createTRPCRouter({
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
await deployLogManagement(
|
||||
ctx.session.activeOrganizationId,
|
||||
ctx.session,
|
||||
input.serverId ?? undefined,
|
||||
input.logProviderIds,
|
||||
);
|
||||
@@ -137,10 +137,7 @@ export const logProviderRouter = createTRPCRouter({
|
||||
removeOnServer: withPermission("logProvider", "create")
|
||||
.input(z.object({ serverId: z.string().nullable().optional() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
await removeLogManagement(
|
||||
ctx.session.activeOrganizationId,
|
||||
input.serverId ?? undefined,
|
||||
);
|
||||
await removeLogManagement(ctx.session, input.serverId ?? undefined);
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "server",
|
||||
|
||||
@@ -158,12 +158,11 @@ export const removeLogProvider = async (logProviderId: string) => {
|
||||
})
|
||||
.where(arrayContains(server.logProviderIds, [logProviderId]));
|
||||
|
||||
const remainingLocalIds = sql`array_remove(${webServerSettings.logProviderIds}, ${logProviderId})`;
|
||||
// Keeps logManagementOrganizationId: the local agent keeps running until it is explicitly removed.
|
||||
await tx
|
||||
.update(webServerSettings)
|
||||
.set({
|
||||
logProviderIds: remainingLocalIds,
|
||||
logManagementOrganizationId: sql`CASE WHEN cardinality(${remainingLocalIds}) = 0 THEN NULL ELSE ${webServerSettings.logManagementOrganizationId} END`,
|
||||
logProviderIds: sql`array_remove(${webServerSettings.logProviderIds}, ${logProviderId})`,
|
||||
})
|
||||
.where(arrayContains(webServerSettings.logProviderIds, [logProviderId]));
|
||||
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import { IS_CLOUD } from "@dokploy/server/constants";
|
||||
import {
|
||||
getAccessibleServerIds,
|
||||
type Server,
|
||||
updateServerLogProviders,
|
||||
} from "@dokploy/server/services/server";
|
||||
import { claimWebServerLogManagement } from "@dokploy/server/services/web-server-settings";
|
||||
import {
|
||||
claimWebServerLogManagement,
|
||||
getWebServerSettings,
|
||||
releaseWebServerLogManagement,
|
||||
} from "@dokploy/server/services/web-server-settings";
|
||||
import {
|
||||
removeVectorAgent,
|
||||
setupVectorAgent,
|
||||
withVectorTargetLock,
|
||||
} from "@dokploy/server/setup/vector-setup";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import {
|
||||
@@ -24,21 +30,45 @@ const assertLocalAgentAllowed = (serverId: string | undefined) => {
|
||||
}
|
||||
};
|
||||
|
||||
const assertServerAccessible = async (
|
||||
serverId: string,
|
||||
session: { userId: string; activeOrganizationId: string },
|
||||
) => {
|
||||
await assertServerBelongsToOrg(serverId, session.activeOrganizationId);
|
||||
const accessibleIds = await getAccessibleServerIds(session);
|
||||
if (!accessibleIds.has(serverId)) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this server",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const deployLogManagement = async (
|
||||
organizationId: string,
|
||||
session: { userId: string; activeOrganizationId: string },
|
||||
serverId: string | undefined,
|
||||
logProviderIds: string[],
|
||||
) => {
|
||||
const organizationId = session.activeOrganizationId;
|
||||
assertLocalAgentAllowed(serverId);
|
||||
await assertServerBelongsToOrg(serverId, organizationId);
|
||||
if (serverId) {
|
||||
await assertServerAccessible(serverId, session);
|
||||
}
|
||||
await assertLogProvidersBelongToOrg(logProviderIds, organizationId);
|
||||
|
||||
if (serverId) {
|
||||
await updateServerLogProviders(serverId, logProviderIds);
|
||||
} else {
|
||||
await withVectorTargetLock(serverId, async () => {
|
||||
if (serverId) {
|
||||
await setupVectorAgent(organizationId, serverId, logProviderIds);
|
||||
await updateServerLogProviders(serverId, logProviderIds);
|
||||
return;
|
||||
}
|
||||
|
||||
const settings = await getWebServerSettings();
|
||||
const previousOwner = settings?.logManagementOrganizationId ?? null;
|
||||
const previousIds = settings?.logProviderIds ?? [];
|
||||
const claimed = await claimWebServerLogManagement(
|
||||
organizationId,
|
||||
logProviderIds,
|
||||
previousOwner ? previousIds : [],
|
||||
);
|
||||
if (!claimed) {
|
||||
throw new TRPCError({
|
||||
@@ -47,38 +77,56 @@ export const deployLogManagement = async (
|
||||
"The local Vector agent is already claimed by another organization",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await setupVectorAgent(organizationId, serverId, logProviderIds);
|
||||
try {
|
||||
await setupVectorAgent(organizationId, undefined, logProviderIds);
|
||||
} catch (error) {
|
||||
if (!previousOwner) {
|
||||
await releaseWebServerLogManagement(organizationId);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
await claimWebServerLogManagement(organizationId, logProviderIds);
|
||||
});
|
||||
};
|
||||
|
||||
export const removeLogManagement = async (
|
||||
organizationId: string,
|
||||
session: { userId: string; activeOrganizationId: string },
|
||||
serverId: string | undefined,
|
||||
) => {
|
||||
const organizationId = session.activeOrganizationId;
|
||||
assertLocalAgentAllowed(serverId);
|
||||
|
||||
if (serverId) {
|
||||
await assertServerBelongsToOrg(serverId, organizationId);
|
||||
await removeVectorAgent(serverId);
|
||||
await updateServerLogProviders(serverId, []);
|
||||
await assertServerAccessible(serverId, session);
|
||||
await withVectorTargetLock(serverId, async () => {
|
||||
await removeVectorAgent(serverId);
|
||||
await updateServerLogProviders(serverId, []);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const released = await claimWebServerLogManagement(organizationId, []);
|
||||
if (!released) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "The local Vector agent is claimed by another organization",
|
||||
});
|
||||
}
|
||||
await removeVectorAgent();
|
||||
await withVectorTargetLock(undefined, async () => {
|
||||
const settings = await getWebServerSettings();
|
||||
const owner = settings?.logManagementOrganizationId;
|
||||
if (owner && owner !== organizationId) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "The local Vector agent is claimed by another organization",
|
||||
});
|
||||
}
|
||||
await removeVectorAgent();
|
||||
await releaseWebServerLogManagement(organizationId);
|
||||
});
|
||||
};
|
||||
|
||||
// Runs even with no providers assigned: deleting the last provider leaves the agent deployed.
|
||||
export const removeServerLogManagement = async (server: Server) => {
|
||||
if (!server.logProviderIds?.length) return;
|
||||
if (!server.sshKeyId) return;
|
||||
try {
|
||||
await removeVectorAgent(server.serverId);
|
||||
await withVectorTargetLock(server.serverId, () =>
|
||||
removeVectorAgent(server.serverId),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[Vector] Failed to remove agent for server ${server.serverId} before deletion:`,
|
||||
|
||||
@@ -1,19 +1,26 @@
|
||||
import { IS_CLOUD } from "@dokploy/server/constants";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { organization } from "@dokploy/server/db/schema";
|
||||
import { findServersWithLogManagementEnabled } from "@dokploy/server/services/server";
|
||||
import { findServersByOrganizationForLogManagement } from "@dokploy/server/services/server";
|
||||
import {
|
||||
claimWebServerLogManagement,
|
||||
getWebServerSettings,
|
||||
releaseWebServerLogManagement,
|
||||
} from "@dokploy/server/services/web-server-settings";
|
||||
import { removeVectorAgent } from "@dokploy/server/setup/vector-setup";
|
||||
import {
|
||||
removeVectorAgent,
|
||||
withVectorTargetLock,
|
||||
} from "@dokploy/server/setup/vector-setup";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
const removeVectorAgents = async (organizationId: string) => {
|
||||
const servers = await findServersWithLogManagementEnabled(organizationId);
|
||||
// Every server, not only those with providers: deleting the last provider leaves the agent deployed.
|
||||
const servers =
|
||||
await findServersByOrganizationForLogManagement(organizationId);
|
||||
await Promise.all(
|
||||
servers.map((server) =>
|
||||
removeVectorAgent(server.serverId).catch((error) => {
|
||||
withVectorTargetLock(server.serverId, () =>
|
||||
removeVectorAgent(server.serverId),
|
||||
).catch((error) => {
|
||||
console.error(
|
||||
`[Vector] Failed to remove the agent on server ${server.serverId} before deleting organization ${organizationId}:`,
|
||||
error,
|
||||
@@ -23,15 +30,17 @@ const removeVectorAgents = async (organizationId: string) => {
|
||||
);
|
||||
|
||||
if (IS_CLOUD) return;
|
||||
const settings = await getWebServerSettings();
|
||||
if (settings?.logManagementOrganizationId !== organizationId) return;
|
||||
await removeVectorAgent().catch((error) => {
|
||||
console.error(
|
||||
`[Vector] Failed to remove the local agent before deleting organization ${organizationId}:`,
|
||||
error,
|
||||
);
|
||||
await withVectorTargetLock(undefined, async () => {
|
||||
const settings = await getWebServerSettings();
|
||||
if (settings?.logManagementOrganizationId !== organizationId) return;
|
||||
await removeVectorAgent().catch((error) => {
|
||||
console.error(
|
||||
`[Vector] Failed to remove the local agent before deleting organization ${organizationId}:`,
|
||||
error,
|
||||
);
|
||||
});
|
||||
await releaseWebServerLogManagement(organizationId);
|
||||
});
|
||||
await claimWebServerLogManagement(organizationId, []);
|
||||
};
|
||||
|
||||
export const deleteOrganization = async (organizationId: string) => {
|
||||
|
||||
@@ -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, desc, eq, isNotNull, sql } from "drizzle-orm";
|
||||
import { and, desc, eq, isNotNull } from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
|
||||
export type Server = typeof server.$inferSelect;
|
||||
@@ -87,18 +87,6 @@ export const findServersByUserId = async (userId: string) => {
|
||||
return servers;
|
||||
};
|
||||
|
||||
export const findServersWithLogManagementEnabled = async (
|
||||
organizationId: string,
|
||||
) => {
|
||||
return await db.query.server.findMany({
|
||||
where: and(
|
||||
eq(server.organizationId, organizationId),
|
||||
sql`array_length(${server.logProviderIds}, 1) > 0`,
|
||||
),
|
||||
columns: { serverId: true },
|
||||
});
|
||||
};
|
||||
|
||||
export const findServersByOrganizationForLogManagement = async (
|
||||
organizationId: string,
|
||||
) => {
|
||||
|
||||
@@ -56,8 +56,7 @@ export const claimWebServerLogManagement = async (
|
||||
.update(webServerSettings)
|
||||
.set({
|
||||
logProviderIds,
|
||||
logManagementOrganizationId:
|
||||
logProviderIds.length > 0 ? organizationId : null,
|
||||
logManagementOrganizationId: organizationId,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(
|
||||
@@ -73,3 +72,14 @@ export const claimWebServerLogManagement = async (
|
||||
|
||||
return updated ?? null;
|
||||
};
|
||||
|
||||
export const releaseWebServerLogManagement = async (organizationId: string) => {
|
||||
await db
|
||||
.update(webServerSettings)
|
||||
.set({
|
||||
logProviderIds: [],
|
||||
logManagementOrganizationId: null,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(webServerSettings.logManagementOrganizationId, organizationId));
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
import {
|
||||
findServerById,
|
||||
findServersByOrganizationForLogManagement,
|
||||
getAccessibleServerIds,
|
||||
} from "@dokploy/server/services/server";
|
||||
import { getWebServerSettings } from "@dokploy/server/services/web-server-settings";
|
||||
import { encodeBase64 } from "@dokploy/server/utils/docker/utils";
|
||||
@@ -315,6 +316,26 @@ const deployVectorService = async (serverId?: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const vectorTargetLocks = new Map<string, Promise<unknown>>();
|
||||
|
||||
// In-process only: Dokploy runs a single dashboard process per host.
|
||||
export const withVectorTargetLock = async <T>(
|
||||
serverId: string | undefined,
|
||||
fn: () => Promise<T>,
|
||||
): Promise<T> => {
|
||||
const key = serverId ?? "local";
|
||||
const previous = vectorTargetLocks.get(key) ?? Promise.resolve();
|
||||
const current = previous.catch(() => {}).then(fn);
|
||||
vectorTargetLocks.set(key, current);
|
||||
try {
|
||||
return await current;
|
||||
} finally {
|
||||
if (vectorTargetLocks.get(key) === current) {
|
||||
vectorTargetLocks.delete(key);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const setupVectorAgent = async (
|
||||
organizationId: string,
|
||||
serverId?: string,
|
||||
@@ -348,39 +369,56 @@ export const removeVectorAgent = async (serverId?: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const isVectorRunning = async (serverId?: string): Promise<boolean> => {
|
||||
type VectorStatus = "running" | "not-running" | "stopped" | "unknown";
|
||||
|
||||
const inspectVectorStatus = async (
|
||||
serverId?: string,
|
||||
): Promise<VectorStatus> => {
|
||||
const docker = await getRemoteDocker(serverId);
|
||||
try {
|
||||
await docker.getService(VECTOR_SERVICE_NAME).inspect();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
const tasks = await docker.listTasks({
|
||||
filters: JSON.stringify({
|
||||
service: [VECTOR_SERVICE_NAME],
|
||||
"desired-state": ["running"],
|
||||
}),
|
||||
});
|
||||
return tasks.some((task) => task.Status?.State === "running")
|
||||
? "running"
|
||||
: "not-running";
|
||||
} catch (error: any) {
|
||||
if (error?.statusCode === 404) {
|
||||
return "stopped";
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const VECTOR_CHECK_TIMEOUT_MS = 5000;
|
||||
|
||||
const vectorStatus = async (
|
||||
serverId: string | null,
|
||||
): Promise<"running" | "stopped" | "unknown"> => {
|
||||
const vectorStatus = async (serverId: string | null): Promise<VectorStatus> => {
|
||||
try {
|
||||
const running = await Promise.race([
|
||||
isVectorRunning(serverId ?? undefined),
|
||||
return await Promise.race([
|
||||
inspectVectorStatus(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([
|
||||
export const getLogManagementServerStatus = async (session: {
|
||||
userId: string;
|
||||
activeOrganizationId: string;
|
||||
}) => {
|
||||
const organizationId = session.activeOrganizationId;
|
||||
const [allServers, settings, accessibleIds] = await Promise.all([
|
||||
findServersByOrganizationForLogManagement(organizationId),
|
||||
IS_CLOUD ? Promise.resolve(null) : getWebServerSettings(),
|
||||
getAccessibleServerIds(session),
|
||||
]);
|
||||
const servers = allServers.filter((s) => accessibleIds.has(s.serverId));
|
||||
|
||||
const webOwnsThisOrg =
|
||||
!!settings &&
|
||||
|
||||
Reference in New Issue
Block a user