From d803fe6123f81eb5869f42d6790bf2fb50758de7 Mon Sep 17 00:00:00 2001 From: Narciso Date: Thu, 10 Sep 2026 12:26:30 -0400 Subject: [PATCH] refactor: clean up log-management code and rebuild migrations --- README.md | 1 + .../log-provider-adapters.test.ts | 356 + .../log-management/log-provider-fetch.test.ts | 96 + .../log-provider-service.test.ts | 280 + .../log-management/vector-config.test.ts | 314 + .../log-management/vector-setup-fixes.test.ts | 244 + .../log-management/vector-setup-web.test.ts | 96 + .../permissions/check-permission.test.ts | 28 + .../enterprise-only-resources.test.ts | 1 + .../permissions/resolve-permissions.test.ts | 1 + .../server/update-server-config.test.ts | 2 + .../__test__/utils/remote-stream.test.ts | 40 +- .../log-management/handle-log-provider.tsx | 347 + .../log-management/show-log-providers.tsx | 247 + .../servers/actions/show-server-actions.tsx | 2 + .../servers/actions/toggle-log-management.tsx | 125 + .../settings/servers/delete-server-modal.tsx | 7 +- .../settings/servers/handle-servers.tsx | 5 +- .../dashboard/settings/web-server.tsx | 2 + apps/dokploy/components/layouts/side.tsx | 8 + .../proprietary/audit-logs/columns.tsx | 1 + .../proprietary/roles/manage-custom-roles.tsx | 13 + apps/dokploy/drizzle/0197_past_omega_red.sql | 19 + apps/dokploy/drizzle/meta/0197_snapshot.json | 9305 +++++++++++++++++ apps/dokploy/drizzle/meta/_journal.json | 7 + apps/dokploy/package.json | 1 + .../dashboard/settings/log-providers.tsx | 55 + apps/dokploy/server/api/root.ts | 2 + .../server/api/routers/log-provider.ts | 141 + .../server/api/routers/organization.ts | 3 + apps/dokploy/server/api/routers/server.ts | 149 +- apps/dokploy/server/api/routers/settings.ts | 71 + apps/dokploy/server/server.ts | 4 + apps/dokploy/server/utils/vector-resync.ts | 249 + docs/log-management.md | 138 + packages/server/package.json | 1 + packages/server/src/constants/index.ts | 3 + packages/server/src/db/schema/audit-log.ts | 3 +- packages/server/src/db/schema/index.ts | 1 + packages/server/src/db/schema/log-provider.ts | 87 + packages/server/src/db/schema/server.ts | 1 + .../src/db/schema/web-server-settings.ts | 6 + packages/server/src/index.ts | 4 + packages/server/src/lib/access-control.ts | 4 + .../providers/aws-cloudwatch.ts | 114 + .../log-management/providers/betterstack.ts | 88 + .../log-management/providers/datadog.ts | 90 + .../log-management/providers/elasticsearch.ts | 153 + .../services/log-management/providers/loki.ts | 119 + .../log-management/providers/registry.ts | 27 + .../log-management/providers/splunk.ts | 80 + .../src/services/log-management/service.ts | 269 + .../src/services/log-management/types.ts | 124 + packages/server/src/services/server.ts | 19 + .../src/services/web-server-settings.ts | 32 +- packages/server/src/setup/vector-setup.ts | 656 ++ pnpm-lock.yaml | 246 + 57 files changed, 14464 insertions(+), 23 deletions(-) create mode 100644 apps/dokploy/__test__/log-management/log-provider-adapters.test.ts create mode 100644 apps/dokploy/__test__/log-management/log-provider-fetch.test.ts create mode 100644 apps/dokploy/__test__/log-management/log-provider-service.test.ts create mode 100644 apps/dokploy/__test__/log-management/vector-config.test.ts create mode 100644 apps/dokploy/__test__/log-management/vector-setup-fixes.test.ts create mode 100644 apps/dokploy/__test__/log-management/vector-setup-web.test.ts create mode 100644 apps/dokploy/components/dashboard/settings/log-management/handle-log-provider.tsx create mode 100644 apps/dokploy/components/dashboard/settings/log-management/show-log-providers.tsx create mode 100644 apps/dokploy/components/dashboard/settings/servers/actions/toggle-log-management.tsx create mode 100644 apps/dokploy/drizzle/0197_past_omega_red.sql create mode 100644 apps/dokploy/drizzle/meta/0197_snapshot.json create mode 100644 apps/dokploy/pages/dashboard/settings/log-providers.tsx create mode 100644 apps/dokploy/server/api/routers/log-provider.ts create mode 100644 apps/dokploy/server/utils/vector-resync.ts create mode 100644 docs/log-management.md create mode 100644 packages/server/src/db/schema/log-provider.ts create mode 100644 packages/server/src/services/log-management/providers/aws-cloudwatch.ts create mode 100644 packages/server/src/services/log-management/providers/betterstack.ts create mode 100644 packages/server/src/services/log-management/providers/datadog.ts create mode 100644 packages/server/src/services/log-management/providers/elasticsearch.ts create mode 100644 packages/server/src/services/log-management/providers/loki.ts create mode 100644 packages/server/src/services/log-management/providers/registry.ts create mode 100644 packages/server/src/services/log-management/providers/splunk.ts create mode 100644 packages/server/src/services/log-management/service.ts create mode 100644 packages/server/src/services/log-management/types.ts create mode 100644 packages/server/src/setup/vector-setup.ts diff --git a/README.md b/README.md index 6a72f10d9..66b827d7c 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Dokploy includes multiple features to make your life easier. - **Templates**: Deploy open-source templates (Plausible, Pocketbase, Calcom, etc.) with a single click. - **Traefik Integration**: Automatically integrates with Traefik for routing and load balancing. - **Real-time Monitoring**: Monitor CPU, memory, storage, and network usage for every resource. +- **Log Management**: Ship container logs to Grafana Loki, Datadog, Better Stack, Elasticsearch/OpenSearch, Splunk, or AWS CloudWatch Logs, tagged by project/environment/application. - **Docker Management**: Easily deploy and manage Docker containers. - **CLI/API**: Manage your applications and databases using the command line or through the API. - **Notifications**: Get notified when your deployments succeed or fail (via Slack, Discord, Telegram, Email, etc.). diff --git a/apps/dokploy/__test__/log-management/log-provider-adapters.test.ts b/apps/dokploy/__test__/log-management/log-provider-adapters.test.ts new file mode 100644 index 000000000..42302bf3d --- /dev/null +++ b/apps/dokploy/__test__/log-management/log-provider-adapters.test.ts @@ -0,0 +1,356 @@ +import { + getLogProviderAdapter, + logProviderAdapters, +} from "@dokploy/server/services/log-management/providers/registry"; +import type { LogProviderRuntimeConfig } from "@dokploy/server/services/log-management/types"; +import { describe, expect, it } from "vitest"; + +const baseConfig: LogProviderRuntimeConfig = { + logProviderId: "log-provider-1", + name: "test", + endpoint: null, + apiKey: null, + apiSecret: null, + extraConfig: null, +}; + +describe("getLogProviderAdapter", () => { + it("returns the registered adapter for each known type", () => { + expect(getLogProviderAdapter("loki").type).toBe("loki"); + expect(getLogProviderAdapter("datadog").type).toBe("datadog"); + expect(getLogProviderAdapter("betterstack").type).toBe("betterstack"); + expect(getLogProviderAdapter("elasticsearch").type).toBe("elasticsearch"); + expect(getLogProviderAdapter("splunk_hec").type).toBe("splunk_hec"); + expect(getLogProviderAdapter("aws_cloudwatch").type).toBe("aws_cloudwatch"); + }); + + it("throws an explicit error for an unregistered type", () => { + // @ts-expect-error: intentionally an unregistered type + expect(() => getLogProviderAdapter("unknown")).toThrow( + /No LogProviderAdapter registered/, + ); + }); +}); + +describe("lokiAdapter.toVectorSink", () => { + it("maps endpoint + labels, without tenant_id if not set", () => { + const adapter = logProviderAdapters.loki; + const sink = adapter.toVectorSink( + { ...baseConfig, endpoint: "https://loki.example.com" }, + "sink_1", + "dokploy_scope", + ); + expect(sink.type).toBe("loki"); + expect(sink.inputs).toEqual(["dokploy_scope"]); + expect(sink.endpoint).toBe("https://loki.example.com"); + expect(sink.labels).toMatchObject({ + dokploy_project: "{{ dokploy_project }}", + dokploy_application: "{{ dokploy_application }}", + dokploy_organization: "{{ dokploy_organization }}", + }); + expect(sink.tenant_id).toBeUndefined(); + expect(sink.buffer).toEqual({ + type: "disk", + max_size: 268_435_488, + when_full: "block", + }); + }); + + it("includes tenant_id when present in extraConfig", () => { + const adapter = logProviderAdapters.loki; + const sink = adapter.toVectorSink( + { + ...baseConfig, + endpoint: "https://loki.example.com", + extraConfig: { tenantId: "tenant-a" }, + }, + "sink_1", + "dokploy_scope", + ); + expect(sink.tenant_id).toBe("tenant-a"); + }); + + it("has no toVectorTransform — goes directly scope -> sink", () => { + expect(logProviderAdapters.loki.toVectorTransform).toBeUndefined(); + }); +}); + +describe("datadogAdapter.toVectorSink", () => { + it("defaults site to datadoghq.com, no `tags` field (datadog_logs no lo soporta)", () => { + const adapter = logProviderAdapters.datadog; + const sink = adapter.toVectorSink( + { ...baseConfig, apiKey: "dd-key" }, + "sink_2", + "transform_2", + ); + expect(sink.type).toBe("datadog_logs"); + expect(sink.default_api_key).toBe("dd-key"); + expect(sink.site).toBe("datadoghq.com"); + expect(sink.tags).toBeUndefined(); + expect(sink.inputs).toEqual(["transform_2"]); + }); + + it("uses extraConfig.site when present", () => { + const adapter = logProviderAdapters.datadog; + const sink = adapter.toVectorSink( + { + ...baseConfig, + apiKey: "dd-key", + extraConfig: { site: "datadoghq.eu" }, + }, + "sink_2", + "transform_2", + ); + expect(sink.site).toBe("datadoghq.eu"); + }); + + it("strips a pasted scheme/trailing slash from extraConfig.site instead of interpolating it as-is", () => { + const adapter = logProviderAdapters.datadog; + const sink = adapter.toVectorSink( + { + ...baseConfig, + apiKey: "dd-key", + extraConfig: { site: "https://datadoghq.eu/" }, + }, + "sink_2", + "transform_2", + ); + expect(sink.site).toBe("datadoghq.eu"); + }); + + it("has toVectorTransform to build .ddtags from the scoping fields — confirmado que datadog_logs no tiene un campo `tags` de config (vector generate-schema)", () => { + const adapter = logProviderAdapters.datadog; + expect(adapter.toVectorTransform).toBeDefined(); + const transform = adapter.toVectorTransform?.( + baseConfig, + "transform_2", + "dokploy_scope", + ); + expect(transform?.type).toBe("remap"); + expect(transform?.inputs).toEqual(["dokploy_scope"]); + expect(transform?.source).toContain(".ddtags"); + }); + + it("escapes commas in scoping field values before building .ddtags, since ddtags is itself comma delimited", () => { + const adapter = logProviderAdapters.datadog; + const transform = adapter.toVectorTransform?.( + baseConfig, + "transform_2", + "dokploy_scope", + ); + expect(transform?.source).toContain( + 'replace(to_string!(.dokploy_project), ",", "_")', + ); + }); +}); + +describe("betterStackAdapter", () => { + it("toVectorTransform renames timestamp to dt, inputs point to scope transform", () => { + const adapter = logProviderAdapters.betterstack; + expect(adapter.toVectorTransform).toBeDefined(); + const transform = adapter.toVectorTransform?.( + baseConfig, + "transform_1", + "dokploy_scope", + ); + expect(transform).toEqual({ + type: "remap", + inputs: ["dokploy_scope"], + source: ".dt = del(.timestamp)", + }); + }); + + it("toVectorSink builds a generic http sink with bearer auth, reading from the transform id", () => { + const adapter = logProviderAdapters.betterstack; + const sink = adapter.toVectorSink( + { + ...baseConfig, + endpoint: "https://in.logs.betterstack.com", + apiKey: "source-token", + }, + "sink_3", + "transform_1", + ); + expect(sink.type).toBe("http"); + expect(sink.inputs).toEqual(["transform_1"]); + expect(sink.uri).toBe("https://in.logs.betterstack.com/"); + expect(sink.auth).toEqual({ strategy: "bearer", token: "source-token" }); + expect(sink.compression).toBe("gzip"); + }); +}); + +describe("elasticsearchAdapter", () => { + it("has a toVectorTransform that flattens .label to a JSON string", () => { + const adapter = logProviderAdapters.elasticsearch; + expect(adapter.toVectorTransform).toBeDefined(); + const transform = adapter.toVectorTransform?.( + baseConfig, + "transform_4", + "dokploy_scope", + ); + expect(transform?.type).toBe("remap"); + expect(transform?.inputs).toEqual(["dokploy_scope"]); + expect(transform?.source).toContain("encode_json(.label)"); + }); + + it("uses basic auth when a username is set", () => { + const adapter = logProviderAdapters.elasticsearch; + const sink = adapter.toVectorSink( + { + ...baseConfig, + endpoint: "https://es.example.com:9200", + apiKey: "secret", + extraConfig: { username: "elastic" }, + }, + "sink_4", + "dokploy_scope", + ); + expect(sink.type).toBe("elasticsearch"); + expect(sink.endpoints).toEqual(["https://es.example.com:9200"]); + expect(sink.auth).toEqual({ + strategy: "basic", + user: "elastic", + password: "secret", + }); + expect(sink.request).toBeUndefined(); + }); + + it("falls back to an ApiKey header when no username is set", () => { + const adapter = logProviderAdapters.elasticsearch; + const sink = adapter.toVectorSink( + { + ...baseConfig, + endpoint: "https://es.example.com:9200", + apiKey: "key-123", + }, + "sink_4", + "dokploy_scope", + ); + expect(sink.auth).toBeUndefined(); + expect(sink.request).toEqual({ + headers: { Authorization: "ApiKey key-123" }, + }); + }); + + it("rejects a username set without a password instead of silently shipping unauthenticated", () => { + const adapter = logProviderAdapters.elasticsearch; + expect(() => + adapter.toVectorSink( + { + ...baseConfig, + endpoint: "https://es.example.com:9200", + extraConfig: { username: "elastic" }, + }, + "sink_4", + "dokploy_scope", + ), + ).toThrow(/Password.*required/i); + }); + + it("includes bulk.index only when extraConfig.index is set", () => { + const adapter = logProviderAdapters.elasticsearch; + const withoutIndex = adapter.toVectorSink( + { ...baseConfig, endpoint: "https://es.example.com:9200" }, + "sink_4", + "dokploy_scope", + ); + expect(withoutIndex.bulk).toBeUndefined(); + const withIndex = adapter.toVectorSink( + { + ...baseConfig, + endpoint: "https://es.example.com:9200", + extraConfig: { index: "dokploy-%Y.%m.%d" }, + }, + "sink_4", + "dokploy_scope", + ); + expect(withIndex.bulk).toEqual({ index: "dokploy-%Y.%m.%d" }); + }); +}); + +describe("splunkAdapter", () => { + it("has no toVectorTransform — scoping fields ride along as plain JSON fields", () => { + expect(logProviderAdapters.splunk_hec.toVectorTransform).toBeUndefined(); + }); + + it("toVectorSink maps endpoint/token, only sets index/sourcetype when present", () => { + const adapter = logProviderAdapters.splunk_hec; + const sink = adapter.toVectorSink( + { + ...baseConfig, + endpoint: "https://splunk.example.com:8088", + apiKey: "hec-token", + }, + "sink_5", + "dokploy_scope", + ); + expect(sink.type).toBe("splunk_hec_logs"); + expect(sink.endpoint).toBe("https://splunk.example.com:8088"); + expect(sink.default_token).toBe("hec-token"); + expect(sink.encoding).toEqual({ codec: "json" }); + expect(sink.index).toBeUndefined(); + expect(sink.sourcetype).toBeUndefined(); + + const withExtras = adapter.toVectorSink( + { + ...baseConfig, + endpoint: "https://splunk.example.com:8088", + apiKey: "hec-token", + extraConfig: { index: "dokploy", sourcetype: "docker" }, + }, + "sink_5", + "dokploy_scope", + ); + expect(withExtras.index).toBe("dokploy"); + expect(withExtras.sourcetype).toBe("docker"); + }); +}); + +describe("awsCloudwatchAdapter", () => { + it("has no toVectorTransform — scoping fields ride along as plain JSON fields", () => { + expect( + logProviderAdapters.aws_cloudwatch.toVectorTransform, + ).toBeUndefined(); + }); + + it("toVectorSink maps access key/secret, region and log group/stream", () => { + const adapter = logProviderAdapters.aws_cloudwatch; + const sink = adapter.toVectorSink( + { + ...baseConfig, + apiKey: "AKIA...", + apiSecret: "shh", + extraConfig: { region: "us-east-1", logGroup: "/dokploy/logs" }, + }, + "sink_6", + "dokploy_scope", + ); + expect(sink.type).toBe("aws_cloudwatch_logs"); + expect(sink.group_name).toBe("/dokploy/logs"); + expect(sink.stream_name).toBe("{{ container_name }}"); + expect(sink.region).toBe("us-east-1"); + expect(sink.auth).toEqual({ + access_key_id: "AKIA...", + secret_access_key: "shh", + }); + }); + + it("uses extraConfig.logStream as the stream template when set", () => { + const adapter = logProviderAdapters.aws_cloudwatch; + const sink = adapter.toVectorSink( + { + ...baseConfig, + apiKey: "AKIA...", + apiSecret: "shh", + extraConfig: { + region: "us-east-1", + logGroup: "/dokploy/logs", + logStream: "{{ dokploy_application }}", + }, + }, + "sink_6", + "dokploy_scope", + ); + expect(sink.stream_name).toBe("{{ dokploy_application }}"); + }); +}); diff --git a/apps/dokploy/__test__/log-management/log-provider-fetch.test.ts b/apps/dokploy/__test__/log-management/log-provider-fetch.test.ts new file mode 100644 index 000000000..060fd1c71 --- /dev/null +++ b/apps/dokploy/__test__/log-management/log-provider-fetch.test.ts @@ -0,0 +1,96 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + lookup: vi.fn(), +})); + +vi.mock("node:dns/promises", () => ({ + lookup: mocks.lookup, +})); + +const { logProviderFetch } = await import( + "@dokploy/server/services/log-management/types" +); + +describe("logProviderFetch — metadata endpoint guard", () => { + const originalFetch = global.fetch; + + beforeEach(() => { + mocks.lookup.mockReset(); + global.fetch = vi.fn().mockResolvedValue(new Response("ok")); + }); + + afterEach(() => { + global.fetch = originalFetch; + }); + + it("rejects a literal AWS/GCP/Azure metadata IP without ever calling fetch", async () => { + await expect( + logProviderFetch("http://169.254.169.254/latest/meta-data/"), + ).rejects.toThrow(/metadata/i); + expect(global.fetch).not.toHaveBeenCalled(); + expect(mocks.lookup).not.toHaveBeenCalled(); + }); + + it("rejects the AWS ECS metadata IPv6 address", async () => { + await expect( + logProviderFetch("http://[fd00:ec2::254]/v2/credentials"), + ).rejects.toThrow(/metadata/i); + expect(global.fetch).not.toHaveBeenCalled(); + }); + + it("rejects every fe80::/10 link-local address, not just literal fe80: ones", async () => { + for (const address of [ + "[fe80::1]", + "[fe90::1]", + "[fea0::1]", + "[febf::1]", + ]) { + await expect( + logProviderFetch(`http://${address}/`), + `expected ${address} to be rejected`, + ).rejects.toThrow(/metadata/i); + } + expect(global.fetch).not.toHaveBeenCalled(); + }); + + it("rejects the GCP metadata hostname without needing DNS resolution", async () => { + await expect( + logProviderFetch("http://metadata.google.internal/computeMetadata/v1/"), + ).rejects.toThrow(/metadata/i); + expect(global.fetch).not.toHaveBeenCalled(); + expect(mocks.lookup).not.toHaveBeenCalled(); + }); + + it("rejects a hostname whose DNS record resolves to a metadata address", async () => { + mocks.lookup.mockResolvedValue([{ address: "169.254.169.254", family: 4 }]); + + await expect( + logProviderFetch("http://attacker-controlled.example.com/"), + ).rejects.toThrow(/metadata/i); + expect(global.fetch).not.toHaveBeenCalled(); + }); + + it("allows a private-network endpoint (self-hosted Loki behind a VPN, say)", async () => { + mocks.lookup.mockResolvedValue([{ address: "10.0.5.20", family: 4 }]); + + await logProviderFetch("http://loki.internal.example.com:3100/ready"); + expect(global.fetch).toHaveBeenCalledTimes(1); + }); + + it("allows a normal public endpoint", async () => { + mocks.lookup.mockResolvedValue([{ address: "203.0.113.10", family: 4 }]); + + await logProviderFetch( + "https://api.us-east-1.datadoghq.com/api/v1/validate", + ); + expect(global.fetch).toHaveBeenCalledTimes(1); + }); + + it("still calls fetch (letting it surface its own error) when DNS resolution fails", async () => { + mocks.lookup.mockRejectedValue(new Error("ENOTFOUND")); + + await logProviderFetch("http://does-not-exist.invalid/"); + expect(global.fetch).toHaveBeenCalledTimes(1); + }); +}); diff --git a/apps/dokploy/__test__/log-management/log-provider-service.test.ts b/apps/dokploy/__test__/log-management/log-provider-service.test.ts new file mode 100644 index 000000000..1a00756c7 --- /dev/null +++ b/apps/dokploy/__test__/log-management/log-provider-service.test.ts @@ -0,0 +1,280 @@ +import { describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + insertValues: vi.fn(), + updateSet: vi.fn(), + findFirst: vi.fn(), +})); + +vi.mock("@dokploy/server/db", () => ({ + db: { + insert: () => ({ + values: (v: any) => ({ + returning: () => Promise.resolve(mocks.insertValues(v)), + }), + }), + update: () => ({ + set: (v: any) => ({ + where: () => ({ + returning: () => Promise.resolve(mocks.updateSet(v)), + }), + }), + }), + query: { + logProvider: { + findFirst: mocks.findFirst, + }, + }, + }, +})); + +const { createLogProvider, updateLogProvider } = await import( + "@dokploy/server/services/log-management/service" +); + +describe("createLogProvider — required credential fields per adapter", () => { + it("rejects a loki provider without endpoint", async () => { + await expect( + createLogProvider( + { name: "loki-prod", providerType: "loki" } as any, + "org-1", + ), + ).rejects.toThrow(/endpoint/i); + expect(mocks.insertValues).not.toHaveBeenCalled(); + }); + + it("rejects a datadog provider without apiKey", async () => { + await expect( + createLogProvider( + { name: "dd", providerType: "datadog" } as any, + "org-1", + ), + ).rejects.toThrow(); + expect(mocks.insertValues).not.toHaveBeenCalled(); + }); + + it("rejects a betterstack provider missing either field", async () => { + await expect( + createLogProvider( + { + name: "bs", + providerType: "betterstack", + apiKey: "token-only", + } as any, + "org-1", + ), + ).rejects.toThrow(); + expect(mocks.insertValues).not.toHaveBeenCalled(); + }); + + it("rejects a splunk_hec provider without a HEC token", async () => { + await expect( + createLogProvider( + { + name: "splunk", + providerType: "splunk_hec", + endpoint: "https://splunk.example.com:8088", + } as any, + "org-1", + ), + ).rejects.toThrow(/token/i); + expect(mocks.insertValues).not.toHaveBeenCalled(); + }); + + it("rejects an aws_cloudwatch provider missing required extraConfig fields (region/log group)", async () => { + await expect( + createLogProvider( + { + name: "cw", + providerType: "aws_cloudwatch", + apiKey: "AKIA...", + apiSecret: "shh", + } as any, + "org-1", + ), + ).rejects.toThrow(/region|log group/i); + expect(mocks.insertValues).not.toHaveBeenCalled(); + }); + + it("rejects a datadog provider whose apiKey only exists inside extraConfig, not the real column", async () => { + await expect( + createLogProvider( + { + name: "dd", + providerType: "datadog", + extraConfig: { apiKey: "sneaky-value" }, + } as any, + "org-1", + ), + ).rejects.toThrow(/api key/i); + expect(mocks.insertValues).not.toHaveBeenCalled(); + }); + + it("rejects an elasticsearch provider with a username but no password (adapter.validateConfig)", async () => { + await expect( + createLogProvider( + { + name: "es", + providerType: "elasticsearch", + endpoint: "https://es.example.com:9200", + extraConfig: { username: "elastic" }, + } as any, + "org-1", + ), + ).rejects.toThrow(/password.*required/i); + expect(mocks.insertValues).not.toHaveBeenCalled(); + }); + + it("accepts an aws_cloudwatch provider with region and log group set via extraConfig", async () => { + mocks.insertValues.mockReturnValue([ + { logProviderId: "lp-2", name: "cw", providerType: "aws_cloudwatch" }, + ]); + const created = await createLogProvider( + { + name: "cw", + providerType: "aws_cloudwatch", + apiKey: "AKIA...", + apiSecret: "shh", + extraConfig: { region: "us-east-1", logGroup: "/dokploy/logs" }, + } as any, + "org-1", + ); + expect(created.logProviderId).toBe("lp-2"); + }); + + it("accepts a loki provider with endpoint", async () => { + mocks.insertValues.mockReturnValue([ + { logProviderId: "lp-1", name: "loki-prod", providerType: "loki" }, + ]); + const created = await createLogProvider( + { + name: "loki-prod", + providerType: "loki", + endpoint: "https://loki.example.com", + } as any, + "org-1", + ); + expect(created.logProviderId).toBe("lp-1"); + expect(mocks.insertValues).toHaveBeenCalled(); + }); +}); + +describe("updateLogProvider — merges partial update with existing row before validating", () => { + it("allows updating just the name without re-sending existing credentials", async () => { + mocks.findFirst.mockResolvedValue({ + logProviderId: "lp-1", + name: "old-name", + providerType: "loki", + endpoint: "https://loki.example.com", + apiKey: null, + apiSecret: null, + extraConfig: null, + }); + mocks.updateSet.mockReturnValue([ + { logProviderId: "lp-1", name: "new-name" }, + ]); + + const updated = await updateLogProvider("lp-1", { name: "new-name" }); + expect(updated.name).toBe("new-name"); + }); + + it("rejects clearing the only required field via update", async () => { + mocks.findFirst.mockResolvedValue({ + logProviderId: "lp-1", + name: "loki-prod", + providerType: "loki", + endpoint: "https://loki.example.com", + apiKey: null, + apiSecret: null, + extraConfig: null, + }); + + await expect(updateLogProvider("lp-1", { endpoint: null })).rejects.toThrow( + /endpoint/i, + ); + }); + + it("clears the old provider's credentials when providerType changes, unless new values are given", async () => { + mocks.findFirst.mockResolvedValue({ + logProviderId: "lp-1", + name: "dd-prod", + providerType: "datadog", + endpoint: null, + apiKey: "old-datadog-key", + apiSecret: null, + extraConfig: { site: "datadoghq.com" }, + }); + mocks.updateSet.mockReturnValue([{ logProviderId: "lp-1" }]); + + await updateLogProvider("lp-1", { + providerType: "loki", + endpoint: "https://loki.example.com", + }); + + expect(mocks.updateSet).toHaveBeenCalledWith( + expect.objectContaining({ + providerType: "loki", + endpoint: "https://loki.example.com", + apiKey: null, + apiSecret: null, + extraConfig: null, + }), + ); + }); + + it("merges a partial extraConfig update instead of replacing it wholesale", async () => { + mocks.findFirst.mockResolvedValue({ + logProviderId: "lp-1", + name: "es-prod", + providerType: "elasticsearch", + endpoint: "https://es.example.com:9200", + apiKey: "secret", + apiSecret: null, + extraConfig: { username: "elastic", index: "custom-index" }, + }); + mocks.updateSet.mockReturnValue([{ logProviderId: "lp-1" }]); + + await updateLogProvider("lp-1", { extraConfig: { username: "new-user" } }); + + expect(mocks.updateSet).toHaveBeenCalledWith( + expect.objectContaining({ + extraConfig: { username: "new-user", index: "custom-index" }, + }), + ); + }); + + it("clears extraConfig entirely when explicitly set to null, instead of merging", async () => { + mocks.findFirst.mockResolvedValue({ + logProviderId: "lp-1", + name: "es-prod", + providerType: "elasticsearch", + endpoint: "https://es.example.com:9200", + apiKey: null, + apiSecret: null, + extraConfig: { username: "elastic" }, + }); + mocks.updateSet.mockReturnValue([{ logProviderId: "lp-1" }]); + + await updateLogProvider("lp-1", { extraConfig: null }); + + expect(mocks.updateSet).toHaveBeenCalledWith( + expect.objectContaining({ extraConfig: null }), + ); + }); + + it("rejects a providerType change that doesn't satisfy the new type's required fields", async () => { + mocks.findFirst.mockResolvedValue({ + logProviderId: "lp-1", + name: "dd-prod", + providerType: "datadog", + endpoint: null, + apiKey: "old-datadog-key", + apiSecret: null, + extraConfig: null, + }); + + await expect( + updateLogProvider("lp-1", { providerType: "loki" }), + ).rejects.toThrow(/endpoint/i); + }); +}); diff --git a/apps/dokploy/__test__/log-management/vector-config.test.ts b/apps/dokploy/__test__/log-management/vector-config.test.ts new file mode 100644 index 000000000..c5e018805 --- /dev/null +++ b/apps/dokploy/__test__/log-management/vector-config.test.ts @@ -0,0 +1,314 @@ +import { describe, expect, it, vi } from "vitest"; +import { parse } from "yaml"; + +const mocks = vi.hoisted(() => ({ + findManyProjects: vi.fn(), + findEnabledLogProvidersByOrganization: vi.fn(), +})); + +vi.mock("@dokploy/server/db", () => ({ + db: { + query: { + projects: { + findMany: mocks.findManyProjects, + }, + }, + }, +})); + +vi.mock( + "@dokploy/server/services/log-management/service", + async (importOriginal) => ({ + ...(await importOriginal< + typeof import("@dokploy/server/services/log-management/service") + >()), + findEnabledLogProvidersByOrganization: + mocks.findEnabledLogProvidersByOrganization, + }), +); + +const { buildVectorConfigYaml } = await import( + "@dokploy/server/setup/vector-setup" +); + +describe("buildVectorConfigYaml", () => { + it("generates a source, a dokploy_scope transform and one sink per enabled provider", async () => { + mocks.findManyProjects.mockResolvedValue([ + { + projectId: "project-1", + name: "My Project", + environments: [ + { + environmentId: "env-1", + name: "production", + applications: [ + { + appName: "app-my-app-abc123", + applicationId: "application-1", + name: "my-app", + }, + ], + compose: [], + }, + ], + }, + ]); + mocks.findEnabledLogProvidersByOrganization.mockResolvedValue([ + { + logProviderId: "log-provider-1", + name: "loki-prod", + providerType: "loki", + endpoint: "https://loki.example.com", + apiKey: null, + apiSecret: null, + extraConfig: null, + }, + ]); + + const yamlStr = await buildVectorConfigYaml("org-1"); + const config = parse(yamlStr) as any; + + expect(config.data_dir).toBe("/var/lib/vector"); + expect(config.sources.docker_logs_source.type).toBe("docker_logs"); + expect(config.transforms.dokploy_scope.type).toBe("remap"); + expect(config.transforms.dokploy_scope.inputs).toEqual([ + "docker_logs_source", + ]); + expect(config.transforms.dokploy_scope.source).toContain( + '.dokploy_organization = "org-1"', + ); + expect(config.transforms.dokploy_scope.source).toContain( + 'app_name == "app-my-app-abc123"', + ); + expect(config.transforms.dokploy_scope.source).toContain( + '.dokploy_project = "My Project"', + ); + expect(config.transforms.dokploy_scope.source).toContain( + '.dokploy_environment = "production"', + ); + expect(config.transforms.dokploy_scope.source).toContain( + '.dokploy_application = "my-app"', + ); + + const sinkIds = Object.keys(config.sinks); + expect(sinkIds).toEqual(["sink_log-provider-1"]); + expect(config.sinks["sink_log-provider-1"].type).toBe("loki"); + expect(config.sinks["sink_log-provider-1"].inputs).toEqual([ + "dokploy_scope", + ]); + }); + + it("chains scope -> transform -> sink for betterstack/datadog, and scope -> sink direct for loki", async () => { + mocks.findManyProjects.mockResolvedValue([]); + mocks.findEnabledLogProvidersByOrganization.mockResolvedValue([ + { + logProviderId: "loki-1", + name: "loki", + providerType: "loki", + endpoint: "https://loki.example.com", + apiKey: null, + apiSecret: null, + extraConfig: null, + }, + { + logProviderId: "bs-1", + name: "betterstack", + providerType: "betterstack", + endpoint: "https://in.logs.betterstack.com", + apiKey: "token", + apiSecret: null, + extraConfig: null, + }, + { + logProviderId: "dd-1", + name: "datadog", + providerType: "datadog", + endpoint: null, + apiKey: "dd-key", + apiSecret: null, + extraConfig: null, + }, + ]); + + const yamlStr = await buildVectorConfigYaml("org-1"); + const config = parse(yamlStr) as any; + + expect(config.sinks["sink_loki-1"].inputs).toEqual(["dokploy_scope"]); + expect(config.transforms["transform_bs-1"]).toBeDefined(); + expect(config.transforms["transform_bs-1"].inputs).toEqual([ + "dokploy_scope", + ]); + expect(config.sinks["sink_bs-1"].inputs).toEqual(["transform_bs-1"]); + expect(config.transforms["transform_dd-1"]).toBeDefined(); + expect(config.transforms["transform_dd-1"].inputs).toEqual([ + "dokploy_scope", + ]); + expect(config.sinks["sink_dd-1"].inputs).toEqual(["transform_dd-1"]); + }); + + it("escapes double quotes in project/application names to avoid breaking the VRL string literal", async () => { + mocks.findManyProjects.mockResolvedValue([ + { + projectId: "project-1", + name: 'My "Project"', + environments: [ + { + environmentId: "env-1", + name: "production", + applications: [ + { + appName: "app-abc123", + applicationId: "application-1", + name: "app", + }, + ], + compose: [], + }, + ], + }, + ]); + mocks.findEnabledLogProvidersByOrganization.mockResolvedValue([]); + + const yamlStr = await buildVectorConfigYaml("org-1"); + const config = parse(yamlStr) as any; + + expect(config.transforms.dokploy_scope.source).toContain( + '.dokploy_project = "My \\"Project\\""', + ); + }); + + it("replaces every control character in a project name, not just newlines, before embedding it in VRL", async () => { + mocks.findManyProjects.mockResolvedValue([ + { + projectId: "project-1", + name: "Weird\r\x00Name", + environments: [ + { + environmentId: "env-1", + name: "production", + applications: [ + { + appName: "app-abc123", + applicationId: "application-1", + name: "app", + }, + ], + compose: [], + }, + ], + }, + ]); + mocks.findEnabledLogProvidersByOrganization.mockResolvedValue([]); + + const yamlStr = await buildVectorConfigYaml("org-1"); + const config = parse(yamlStr) as any; + const source = config.transforms.dokploy_scope.source as string; + + expect(source).toContain('.dokploy_project = "Weird Name"'); + }); + + it("keeps events without a matching appName unscoped (no crash, no filtering), with the scoping fields defaulted to empty string", async () => { + mocks.findManyProjects.mockResolvedValue([]); + mocks.findEnabledLogProvidersByOrganization.mockResolvedValue([]); + + const yamlStr = await buildVectorConfigYaml("org-1"); + const config = parse(yamlStr) as any; + + expect(config.transforms.dokploy_scope.source).toBe( + [ + '.dokploy_organization = "org-1"', + '.dokploy_project = ""', + '.dokploy_project_id = ""', + '.dokploy_environment = ""', + '.dokploy_environment_id = ""', + '.dokploy_application = ""', + '.dokploy_application_id = ""', + ].join("\n"), + ); + }); + + it("uses preloaded org data instead of querying, when given (fan-out reuse)", async () => { + mocks.findManyProjects.mockClear(); + mocks.findEnabledLogProvidersByOrganization.mockClear(); + + const yamlStr = await buildVectorConfigYaml("org-1", { + providers: [], + lookup: {}, + }); + + expect(mocks.findManyProjects).not.toHaveBeenCalled(); + expect(mocks.findEnabledLogProvidersByOrganization).not.toHaveBeenCalled(); + const config = parse(yamlStr) as any; + expect(config.sinks).toEqual({}); + }); + + it("skips a provider whose config an adapter rejects at build time, instead of failing the whole org's config", async () => { + const yamlStr = await buildVectorConfigYaml("org-1", { + providers: [ + { + logProviderId: "broken-es", + name: "es-broken", + providerType: "elasticsearch", + endpoint: "https://es.example.com:9200", + apiKey: null, + apiSecret: null, + extraConfig: { username: "elastic" }, + }, + { + logProviderId: "good-loki", + name: "loki-ok", + providerType: "loki", + endpoint: "https://loki.example.com", + apiKey: null, + apiSecret: null, + extraConfig: null, + }, + ] as any, + lookup: {}, + }); + + const config = parse(yamlStr) as any; + expect(Object.keys(config.sinks)).toEqual(["sink_good-loki"]); + }); + + it("dropUnmatched (local host only) filters out events with no matching Dokploy app before any sink", async () => { + const yamlStr = await buildVectorConfigYaml( + "org-1", + { + providers: [ + { + logProviderId: "loki-1", + name: "loki", + providerType: "loki", + endpoint: "https://loki.example.com", + apiKey: null, + apiSecret: null, + extraConfig: null, + }, + ] as any, + lookup: {}, + }, + { dropUnmatched: true }, + ); + + const config = parse(yamlStr) as any; + expect(config.transforms.dokploy_scope_local_only).toEqual({ + type: "filter", + inputs: ["dokploy_scope"], + condition: '.dokploy_project != ""', + }); + expect(config.sinks["sink_loki-1"].inputs).toEqual([ + "dokploy_scope_local_only", + ]); + }); + + it("does not add the drop-unmatched filter for the per-server (non-local) path", async () => { + mocks.findManyProjects.mockResolvedValue([]); + mocks.findEnabledLogProvidersByOrganization.mockResolvedValue([]); + + const yamlStr = await buildVectorConfigYaml("org-1"); + const config = parse(yamlStr) as any; + + expect(config.transforms.dokploy_scope_local_only).toBeUndefined(); + }); +}); diff --git a/apps/dokploy/__test__/log-management/vector-setup-fixes.test.ts b/apps/dokploy/__test__/log-management/vector-setup-fixes.test.ts new file mode 100644 index 000000000..e76568a8e --- /dev/null +++ b/apps/dokploy/__test__/log-management/vector-setup-fixes.test.ts @@ -0,0 +1,244 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + reserve: vi.fn(), +})); + +vi.mock("@dokploy/server/db", () => ({ + db: { + query: { projects: { findMany: vi.fn() } }, + }, +})); + +vi.mock("postgres", () => ({ + default: vi.fn(() => ({ reserve: mocks.reserve })), +})); + +const makeReservedMock = (locked: boolean) => { + const tag: any = vi + .fn() + .mockImplementation((strings: TemplateStringsArray) => { + const text = strings.join(""); + if (text.includes("pg_try_advisory_lock")) { + return Promise.resolve([{ locked }]); + } + return Promise.resolve([]); + }); + tag.release = vi.fn(); + return tag; +}; + +const { + withConfigWriteLock, + collectSecretValues, + redactSecrets, + stripImageDigest, + vectorServiceSpecUnchanged, + buildServiceSettings, +} = await import("@dokploy/server/setup/vector-setup"); + +describe("withConfigWriteLock — cross-process guard (session scoped pg_advisory_lock)", () => { + beforeEach(() => { + mocks.reserve.mockReset(); + }); + + it("runs fn and returns its result when the advisory lock is acquired", async () => { + const reserved = makeReservedMock(true); + mocks.reserve.mockResolvedValue(reserved); + const fn = vi.fn().mockResolvedValue("ok"); + + await expect(withConfigWriteLock("server-1", fn)).resolves.toBe("ok"); + expect(fn).toHaveBeenCalledTimes(1); + const joinedCalls = reserved.mock.calls.map((call: any[]) => + (call[0] as string[]).join(""), + ); + expect( + joinedCalls.some((text: string) => text.includes("pg_advisory_unlock")), + ).toBe(true); + expect(reserved.release).toHaveBeenCalledTimes(1); + }); + + it("retries, then throws without ever calling fn, when another process keeps holding the lock", async () => { + mocks.reserve.mockImplementation(async () => makeReservedMock(false)); + const fn = vi.fn(); + + await expect(withConfigWriteLock("server-1", fn)).rejects.toThrow( + /already in progress/i, + ); + expect(fn).not.toHaveBeenCalled(); + expect(mocks.reserve).toHaveBeenCalledTimes(3); + }, 10_000); + + it("still serializes two calls for the same serverId within this process", async () => { + mocks.reserve.mockImplementation(async () => makeReservedMock(true)); + const order: string[] = []; + const slow = async (label: string, ms: number) => { + order.push(`${label}:start`); + await new Promise((resolve) => setTimeout(resolve, ms)); + order.push(`${label}:end`); + return label; + }; + + const first = withConfigWriteLock("server-a", () => slow("first", 20)); + const second = withConfigWriteLock("server-a", () => slow("second", 1)); + await Promise.all([first, second]); + + expect(order).toEqual([ + "first:start", + "first:end", + "second:start", + "second:end", + ]); + }); +}); + +describe("collectSecretValues / redactSecrets", () => { + it("collects endpoint/apiKey/apiSecret and string extraConfig values across all providers", () => { + const providers = [ + { + endpoint: "https://loki.example.com", + apiKey: "loki-key", + apiSecret: null, + extraConfig: { tenantId: "tenant-a" }, + }, + { + endpoint: null, + apiKey: "dd-key", + apiSecret: null, + extraConfig: { site: "datadoghq.eu" }, + }, + ] as any; + + expect(collectSecretValues(providers)).toEqual( + expect.arrayContaining([ + "https://loki.example.com", + "loki-key", + "tenant-a", + "dd-key", + "datadoghq.eu", + ]), + ); + }); + + it("ignores non-string extraConfig values and null/empty credentials", () => { + const providers = [ + { + endpoint: null, + apiKey: null, + apiSecret: null, + extraConfig: { limit: 10, enabled: true, empty: "" }, + }, + ] as any; + expect(collectSecretValues(providers)).toEqual([]); + }); + + it("redacts every collected secret, longest first so a short one can't eat part of a longer one", () => { + const providers = [ + { + endpoint: null, + apiKey: "hunter2-long-suffix", + apiSecret: null, + extraConfig: { other: "hunter2" }, + }, + ] as any; + const text = + "sink validation error near apiKey=hunter2-long-suffix and hunter2"; + + const redacted = redactSecrets(text, collectSecretValues(providers)); + + expect(redacted).not.toContain("hunter2"); + expect(redacted).toBe( + "sink validation error near apiKey=[redacted] and [redacted]", + ); + }); +}); + +describe("stripImageDigest", () => { + it("drops an @sha256 digest suffix", () => { + expect( + stripImageDigest("timberio/vector:latest-alpine@sha256:abc123"), + ).toBe("timberio/vector:latest-alpine"); + }); + + it("returns the image unchanged when there's no digest", () => { + expect(stripImageDigest("timberio/vector:latest-alpine")).toBe( + "timberio/vector:latest-alpine", + ); + }); +}); + +describe("vectorServiceSpecUnchanged", () => { + const settings = { + TaskTemplate: { + ContainerSpec: { + Image: "timberio/vector:latest-alpine", + Args: ["--config", "/etc/vector/vector.yaml", "--watch-config"], + Mounts: [{ Type: "bind", Source: "/a", Target: "/b" }], + }, + Networks: [{ Target: "host" }], + }, + Mode: { Replicated: { Replicas: 1 } }, + } as any; + + it("is unchanged when inspect matches settings modulo an image digest suffix and a resolved network ID", () => { + const inspect = { + Spec: { + TaskTemplate: { + ContainerSpec: { + Image: "timberio/vector:latest-alpine@sha256:deadbeef", + Args: ["--config", "/etc/vector/vector.yaml", "--watch-config"], + Mounts: [{ Type: "bind", Source: "/a", Target: "/b" }], + }, + Networks: [{ Target: "k1vw5h40twado5nnk9n2hi8nt" }], + }, + Mode: { Replicated: { Replicas: 1 } }, + }, + } as any; + expect(vectorServiceSpecUnchanged(inspect, settings)).toBe(true); + }); + + it("is changed when a bind mount source differs", () => { + const inspect = { + Spec: { + TaskTemplate: { + ContainerSpec: { + Image: "timberio/vector:latest-alpine", + Args: ["--config", "/etc/vector/vector.yaml", "--watch-config"], + Mounts: [{ Type: "bind", Source: "/old-path", Target: "/b" }], + }, + Networks: [{ Target: "host" }], + }, + Mode: { Replicated: { Replicas: 1 } }, + }, + } as any; + expect(vectorServiceSpecUnchanged(inspect, settings)).toBe(false); + }); + + it("is changed when Mode differs", () => { + const inspect = { + Spec: { + TaskTemplate: settings.TaskTemplate, + Mode: { Global: {} }, + }, + } as any; + expect(vectorServiceSpecUnchanged(inspect, settings)).toBe(false); + }); +}); + +describe("buildServiceSettings", () => { + it("bind-mounts the config directory, not the vector.yaml file itself", () => { + const settings = buildServiceSettings("/etc/dokploy/vector") as any; + const mounts = settings.TaskTemplate.ContainerSpec.Mounts as Array<{ + Source: string; + Target: string; + }>; + const configMount = mounts.find((m) => m.Target === "/etc/vector"); + expect(configMount?.Source).toBe("/etc/dokploy/vector"); + expect(mounts.some((m) => m.Target === "/etc/vector/vector.yaml")).toBe( + false, + ); + expect(settings.TaskTemplate.ContainerSpec.Args).toContain( + "/etc/vector/vector.yaml", + ); + }); +}); diff --git a/apps/dokploy/__test__/log-management/vector-setup-web.test.ts b/apps/dokploy/__test__/log-management/vector-setup-web.test.ts new file mode 100644 index 000000000..9e2d12bfd --- /dev/null +++ b/apps/dokploy/__test__/log-management/vector-setup-web.test.ts @@ -0,0 +1,96 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + getWebServerSettings: vi.fn(), + hasEnabledLogProvider: vi.fn(), + getService: vi.fn(), + remove: vi.fn(), +})); + +vi.mock("@dokploy/server/services/web-server-settings", () => ({ + getWebServerSettings: mocks.getWebServerSettings, +})); + +vi.mock( + "@dokploy/server/services/log-management/service", + async (importOriginal) => ({ + ...(await importOriginal< + typeof import("@dokploy/server/services/log-management/service") + >()), + hasEnabledLogProvider: mocks.hasEnabledLogProvider, + }), +); + +vi.mock("@dokploy/server/utils/servers/remote-docker", () => ({ + getRemoteDocker: vi.fn().mockResolvedValue({ + getService: mocks.getService, + }), +})); + +const { syncWebVectorAgent } = await import( + "@dokploy/server/setup/vector-setup" +); + +describe("syncWebVectorAgent — install condition", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("tears down (never installs) when the local toggle is off", async () => { + mocks.getWebServerSettings.mockResolvedValue({ + enableLogManagement: false, + logManagementOrganizationId: "org-1", + }); + mocks.remove.mockResolvedValue(undefined); + mocks.getService.mockReturnValue({ remove: mocks.remove }); + + const result = await syncWebVectorAgent(); + + expect(result.installed).toBe(false); + expect(mocks.getService).toHaveBeenCalledWith("dokploy-vector"); + expect(mocks.remove).toHaveBeenCalled(); + }); + + it("tears down when no organization has claimed the local agent yet", async () => { + mocks.getWebServerSettings.mockResolvedValue({ + enableLogManagement: true, + logManagementOrganizationId: null, + }); + mocks.remove.mockResolvedValue(undefined); + mocks.getService.mockReturnValue({ remove: mocks.remove }); + + const result = await syncWebVectorAgent(); + + expect(result.installed).toBe(false); + expect(mocks.hasEnabledLogProvider).not.toHaveBeenCalled(); + }); + + it("tears down when the owning organization has no enabled log provider", async () => { + mocks.getWebServerSettings.mockResolvedValue({ + enableLogManagement: true, + logManagementOrganizationId: "org-1", + }); + mocks.hasEnabledLogProvider.mockResolvedValue(false); + mocks.remove.mockResolvedValue(undefined); + mocks.getService.mockReturnValue({ remove: mocks.remove }); + + const result = await syncWebVectorAgent(); + + expect(result.installed).toBe(false); + expect(mocks.hasEnabledLogProvider).toHaveBeenCalledWith("org-1"); + }); + + it("skips the hasEnabledLogProvider query when providers are already preloaded", async () => { + mocks.getWebServerSettings.mockResolvedValue({ + enableLogManagement: true, + logManagementOrganizationId: "org-1", + }); + mocks.remove.mockResolvedValue(undefined); + mocks.getService.mockReturnValue({ remove: mocks.remove }); + + const result = await syncWebVectorAgent({ providers: [], lookup: {} }); + + expect(result.installed).toBe(false); + expect(mocks.hasEnabledLogProvider).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/dokploy/__test__/permissions/check-permission.test.ts b/apps/dokploy/__test__/permissions/check-permission.test.ts index ee0e61d7a..4e34afa51 100644 --- a/apps/dokploy/__test__/permissions/check-permission.test.ts +++ b/apps/dokploy/__test__/permissions/check-permission.test.ts @@ -73,6 +73,20 @@ describe("owner and admin bypass enterprise resources", () => { ).resolves.toBeUndefined(); }); + it("owner bypasses logProvider.read", async () => { + memberToReturn = mockMemberData("owner"); + await expect( + checkPermission(ctx, { logProvider: ["read"] }), + ).resolves.toBeUndefined(); + }); + + it("admin bypasses logProvider.create", async () => { + memberToReturn = mockMemberData("admin"); + await expect( + checkPermission(ctx, { logProvider: ["create"] }), + ).resolves.toBeUndefined(); + }); + it("owner bypasses multiple enterprise permissions at once", async () => { memberToReturn = mockMemberData("owner"); await expect( @@ -139,6 +153,20 @@ describe("member is denied org-level enterprise resources (CVE: bypass via stati checkPermission(ctx, { registry: ["create"] }), ).rejects.toThrow(); }); + + it("member is denied logProvider.read", async () => { + memberToReturn = mockMemberData("member"); + await expect( + checkPermission(ctx, { logProvider: ["read"] }), + ).rejects.toThrow(); + }); + + it("member is denied logProvider.create", async () => { + memberToReturn = mockMemberData("member"); + await expect( + checkPermission(ctx, { logProvider: ["create"] }), + ).rejects.toThrow(); + }); }); describe("static roles validate free-tier resources", () => { diff --git a/apps/dokploy/__test__/permissions/enterprise-only-resources.test.ts b/apps/dokploy/__test__/permissions/enterprise-only-resources.test.ts index 970c24d07..2533b8e3b 100644 --- a/apps/dokploy/__test__/permissions/enterprise-only-resources.test.ts +++ b/apps/dokploy/__test__/permissions/enterprise-only-resources.test.ts @@ -18,6 +18,7 @@ const FREE_TIER_RESOURCES = [ "gitProviders", "traefikFiles", "api", + "logProvider", ]; const ENTERPRISE_RESOURCES = [ diff --git a/apps/dokploy/__test__/permissions/resolve-permissions.test.ts b/apps/dokploy/__test__/permissions/resolve-permissions.test.ts index b78627ea9..ddf5e8e6f 100644 --- a/apps/dokploy/__test__/permissions/resolve-permissions.test.ts +++ b/apps/dokploy/__test__/permissions/resolve-permissions.test.ts @@ -111,6 +111,7 @@ describe("enterprise resources for static roles", () => { expect(perms.destination.read).toBe(false); expect(perms.notification.read).toBe(false); expect(perms.auditLog.read).toBe(false); + expect(perms.logProvider.read).toBe(false); }); }); diff --git a/apps/dokploy/__test__/traefik/server/update-server-config.test.ts b/apps/dokploy/__test__/traefik/server/update-server-config.test.ts index d1a03fedc..9d43cec32 100644 --- a/apps/dokploy/__test__/traefik/server/update-server-config.test.ts +++ b/apps/dokploy/__test__/traefik/server/update-server-config.test.ts @@ -25,6 +25,8 @@ const baseSettings: WebServerSettings = { letsEncryptEmail: null, sshPrivateKey: null, enableDockerCleanup: false, + enableLogManagement: false, + logManagementOrganizationId: null, buildsConcurrency: 1, logCleanupCron: null, metricsConfig: { diff --git a/apps/dokploy/__test__/utils/remote-stream.test.ts b/apps/dokploy/__test__/utils/remote-stream.test.ts index 6f7a3b358..f5cf034de 100644 --- a/apps/dokploy/__test__/utils/remote-stream.test.ts +++ b/apps/dokploy/__test__/utils/remote-stream.test.ts @@ -40,21 +40,29 @@ describe("pipeBetweenServers", () => { await fs.rm(dir, { recursive: true, force: true }); }, 30_000); - it("reports a target failure with its stderr", async () => { - await expect( - pipeBetweenServers({ - source: { serverId: null, command: "printf x" }, - target: { serverId: null, command: "echo boom >&2; exit 3" }, - }), - ).rejects.toThrow("target exited with code 3: boom"); - }); + it( + "reports a target failure with its stderr", + async () => { + await expect( + pipeBetweenServers({ + source: { serverId: null, command: "printf x" }, + target: { serverId: null, command: "echo boom >&2; exit 3" }, + }), + ).rejects.toThrow("target exited with code 3: boom"); + }, + 30_000, + ); - it("reports a source failure", async () => { - await expect( - pipeBetweenServers({ - source: { serverId: null, command: "exit 2" }, - target: { serverId: null, command: "cat > /dev/null" }, - }), - ).rejects.toThrow("source exited with code 2"); - }); + it( + "reports a source failure", + async () => { + await expect( + pipeBetweenServers({ + source: { serverId: null, command: "exit 2" }, + target: { serverId: null, command: "cat > /dev/null" }, + }), + ).rejects.toThrow("source exited with code 2"); + }, + 30_000, + ); }); diff --git a/apps/dokploy/components/dashboard/settings/log-management/handle-log-provider.tsx b/apps/dokploy/components/dashboard/settings/log-management/handle-log-provider.tsx new file mode 100644 index 000000000..d134aaa99 --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/log-management/handle-log-provider.tsx @@ -0,0 +1,347 @@ +import type { LogProviderType } from "@dokploy/server/services/log-management/types"; +import { AlertTriangle, PenBoxIcon, PlusIcon } from "lucide-react"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; +import { api } from "@/utils/api"; + +const TOP_LEVEL_KEYS = new Set(["endpoint", "apiKey", "apiSecret"]); + +interface Props { + logProviderId?: string; +} + +export const HandleLogProvider = ({ logProviderId }: Props) => { + const utils = api.useUtils(); + const [isOpen, setIsOpen] = useState(false); + const [name, setName] = useState(""); + const [providerType, setProviderType] = useState(""); + const [enabled, setEnabled] = useState(true); + const [fieldValues, setFieldValues] = useState>({}); + const [initialFieldValues, setInitialFieldValues] = useState< + Record + >({}); + + const { data: availableTypes } = api.logProvider.availableTypes.useQuery(); + const { data: provider } = api.logProvider.one.useQuery( + { logProviderId: logProviderId || "" }, + { enabled: !!logProviderId }, + ); + + const { + mutateAsync, + isPending: isSaving, + error, + isError, + } = logProviderId + ? api.logProvider.update.useMutation() + : api.logProvider.create.useMutation(); + const { + mutateAsync: testConnection, + isPending: isTestingRaw, + error: testError, + isError: testIsError, + } = api.logProvider.testConnection.useMutation(); + const { + mutateAsync: testConnectionById, + isPending: isTestingById, + error: testByIdError, + isError: testByIdIsError, + } = api.logProvider.testConnectionById.useMutation(); + + const selectedType = availableTypes?.find((t) => t.type === providerType); + + const handleProviderTypeChange = (value: string) => { + setProviderType(value); + setFieldValues({}); + }; + + useEffect(() => { + if (provider) { + setName(provider.name); + setProviderType(provider.providerType); + setEnabled(provider.enabled); + const extraConfig = (provider.extraConfig ?? {}) as Record< + string, + string + >; + setFieldValues(extraConfig); + setInitialFieldValues(extraConfig); + } else if (isOpen) { + setName(""); + setProviderType(""); + setEnabled(true); + setFieldValues({}); + setInitialFieldValues({}); + } + }, [provider, isOpen]); + + const buildPayload = () => { + const payload: Record = { name, providerType, enabled }; + const extraConfig: Record = {}; + let hasExtraConfigFields = false; + for (const field of selectedType?.credentialFields ?? []) { + const value = fieldValues[field.key]; + if (TOP_LEVEL_KEYS.has(field.key)) { + if (value) { + payload[field.key] = value; + } + } else { + hasExtraConfigFields = true; + extraConfig[field.key] = value ?? ""; + } + } + if (hasExtraConfigFields) { + payload.extraConfig = extraConfig; + } + return payload; + }; + + const topLevelKeys = (selectedType?.credentialFields ?? []) + .filter((field) => TOP_LEVEL_KEYS.has(field.key)) + .map((field) => field.key); + const touchedTopLevelKeys = topLevelKeys.filter((key) => !!fieldValues[key]); + const extraConfigKeys = (selectedType?.credentialFields ?? []) + .filter((field) => !TOP_LEVEL_KEYS.has(field.key)) + .map((field) => field.key); + const touchedExtraConfigKeys = extraConfigKeys.filter( + (key) => (fieldValues[key] ?? "") !== (initialFieldValues[key] ?? ""), + ); + const touchedAnyCredentialField = + touchedTopLevelKeys.length > 0 || touchedExtraConfigKeys.length > 0; + const isPartiallyTouched = + !!logProviderId && + touchedAnyCredentialField && + touchedTopLevelKeys.length < topLevelKeys.length; + + const onTest = async () => { + if (logProviderId && !touchedAnyCredentialField) { + await testConnectionById({ logProviderId }) + .then((result) => { + if (result.warning) { + toast.message(result.warning); + } else { + toast.success("Connection tested successfully"); + } + }) + .catch((e) => { + toast.error( + e instanceof Error ? e.message : "Connection test failed", + ); + }); + return; + } + const payload = buildPayload(); + await testConnection({ + name: payload.name as string, + providerType: payload.providerType as LogProviderType, + enabled: payload.enabled as boolean, + endpoint: (payload.endpoint as string) ?? null, + apiKey: (payload.apiKey as string) ?? null, + apiSecret: (payload.apiSecret as string) ?? null, + extraConfig: (payload.extraConfig as Record) ?? null, + }) + .then((result) => { + if (result.warning) { + toast.message(result.warning); + } else { + toast.success("Connection tested successfully"); + } + }) + .catch((e) => { + toast.error(e instanceof Error ? e.message : "Connection test failed"); + }); + }; + + const onSubmit = async () => { + const payload = buildPayload(); + await mutateAsync({ + ...(logProviderId ? { logProviderId } : {}), + ...payload, + } as any) + .then((result: any) => { + utils.logProvider.all.invalidate(); + toast.success( + logProviderId ? "Log provider updated" : "Log provider added", + ); + if (result?.syncErrors && result.syncErrors.length > 0) { + toast.error( + `Failed to sync ${result.syncErrors.length} server(s) — they may still be shipping with the old config`, + ); + } + setIsOpen(false); + }) + .catch(() => { + toast.error( + logProviderId + ? "Error updating log provider" + : "Error adding log provider", + ); + }); + }; + + const isTesting = isTestingRaw || isTestingById; + + return ( + + + {logProviderId ? ( + + ) : ( + + )} + + + + + {logProviderId ? "Edit Log Provider" : "Add a Log Provider"} + + + Vector will ship container logs from every server with Log + Management enabled to this provider. + + + {(isError || testIsError || testByIdIsError) && ( +
+ + + {testError?.message || + testByIdError?.message || + error?.message || + ""} + +
+ )} +
+
+ + setName(e.target.value)} + /> +
+
+ + +
+ + {selectedType?.credentialFields.map((field) => ( +
+ + {field.helpText && ( + + {field.helpText} + + )} + + setFieldValues((prev) => ({ + ...prev, + [field.key]: e.target.value, + })) + } + /> +
+ ))} + +
+ + +
+
+ + + {isPartiallyTouched && ( + + You changed something that needs a fresh credential to test — fill + in every credential field to test with the new values, or revert + it to test with what's already saved. + + )} +
+ + +
+
+
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/settings/log-management/show-log-providers.tsx b/apps/dokploy/components/dashboard/settings/log-management/show-log-providers.tsx new file mode 100644 index 000000000..626045bfb --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/log-management/show-log-providers.tsx @@ -0,0 +1,247 @@ +import { + AlertTriangle, + CheckCircle2, + Loader2, + ScrollText, + Trash2, +} from "lucide-react"; +import Link from "next/link"; +import { toast } from "sonner"; +import { DialogAction } from "@/components/shared/dialog-action"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { api } from "@/utils/api"; +import { HandleLogProvider } from "./handle-log-provider"; + +interface LogProviderRowProps { + provider: { + logProviderId: string; + name: string; + enabled: boolean; + providerType: string; + }; + index: number; + typeLabel: string; + canEdit: boolean; + canDelete: boolean; + onDeleted: () => void; +} + +const LogProviderRow = ({ + provider, + index, + typeLabel, + canEdit, + canDelete, + onDeleted, +}: LogProviderRowProps) => { + const { mutateAsync, isPending: isRemoving } = + api.logProvider.remove.useMutation(); + const { mutateAsync: testConnectionById, isPending: isTesting } = + api.logProvider.testConnectionById.useMutation(); + + return ( +
+
+
+
+ + {index + 1}. {provider.name} + + {provider.enabled ? "Enabled" : "Disabled"} + + +
{typeLabel}
+
+
+ +
+ {canEdit && ( + + )} + {canEdit && ( + + )} + + {canDelete && ( + { + await mutateAsync({ + logProviderId: provider.logProviderId, + }) + .then((result) => { + toast.success("Log provider deleted successfully"); + if (result.syncErrors && result.syncErrors.length > 0) { + toast.error( + `Failed to sync ${result.syncErrors.length} server(s) — they may still be shipping with the old config`, + ); + } + onDeleted(); + }) + .catch(() => { + toast.error("Error deleting log provider"); + }); + }} + > + + + )} +
+
+
+ ); +}; + +export const ShowLogProviders = () => { + const utils = api.useUtils(); + const { data, isPending, refetch } = api.logProvider.all.useQuery(); + const { data: availableTypes } = api.logProvider.availableTypes.useQuery(); + const { data: permissions } = api.user.getPermissions.useQuery(); + const { data: isCloud } = api.settings.isCloud.useQuery(); + const { data: webServerSettings } = + api.settings.getWebServerSettings.useQuery(); + const { data: servers } = api.server.all.useQuery(); + const providerListLabel = availableTypes?.length + ? ` (${availableTypes.map((t) => t.label).join(", ")})` + : ""; + + const hasAnyProvider = (data?.length ?? 0) > 0; + const knowsAboutServers = servers !== undefined; + const hasActiveTarget = + !!webServerSettings?.enableLogManagement || + !!servers?.some((s) => s.enableLogManagement); + const showNoActiveTargetBanner = + hasAnyProvider && knowsAboutServers && !hasActiveTarget; + + return ( +
+ +
+ + + + Log Management + + + {`Ship container logs to an external provider${providerListLabel}. Needs at least one provider here and the toggle enabled on each server's settings.`} + + + + {showNoActiveTargetBanner && ( +
+ +
+ + Nothing will ship yet — no server has Log Management turned + on. + + +
+
+ )} + {isPending ? ( +
+ Loading... + +
+ ) : ( + <> + {data?.length === 0 ? ( +
+ + + You don't have any log provider configured + + {permissions?.logProvider.create && } +
+ ) : ( +
+
+ {data?.map((provider, index) => ( + t.type === provider.providerType, + )?.label ?? provider.providerType + } + canEdit={!!permissions?.logProvider.create} + canDelete={!!permissions?.logProvider.delete} + onDeleted={() => { + refetch(); + utils.server.all?.invalidate?.(); + }} + /> + ))} +
+ + {permissions?.logProvider.create && ( +
+ +
+ )} +
+ )} + + )} +
+
+
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/settings/servers/actions/show-server-actions.tsx b/apps/dokploy/components/dashboard/settings/servers/actions/show-server-actions.tsx index 334d25b20..74ada5406 100644 --- a/apps/dokploy/components/dashboard/settings/servers/actions/show-server-actions.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/actions/show-server-actions.tsx @@ -12,6 +12,7 @@ import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; import { ShowStorageActions } from "./show-storage-actions"; import { ShowTraefikActions } from "./show-traefik-actions"; import { ToggleDockerCleanup } from "./toggle-docker-cleanup"; +import { ToggleLogManagement } from "./toggle-log-management"; interface Props { serverId: string; @@ -49,6 +50,7 @@ export const ShowServerActions = ({ serverId, asButton = false }: Props) => { + diff --git a/apps/dokploy/components/dashboard/settings/servers/actions/toggle-log-management.tsx b/apps/dokploy/components/dashboard/settings/servers/actions/toggle-log-management.tsx new file mode 100644 index 000000000..0ef88e4d2 --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/servers/actions/toggle-log-management.tsx @@ -0,0 +1,125 @@ +import { HelpCircle, Loader2 } from "lucide-react"; +import { toast } from "sonner"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { api } from "@/utils/api"; + +interface Props { + serverId?: string; +} + +export const ToggleLogManagement = ({ serverId }: Props) => { + const utils = api.useUtils(); + const { data: server } = api.server.one.useQuery( + { serverId: serverId || "" }, + { enabled: !!serverId }, + ); + const { data: webServerSettings } = + api.settings.getWebServerSettings.useQuery(undefined, { + enabled: !serverId, + }); + const { mutateAsync: updateServer, isPending: isServerPending } = + api.server.updateLogManagement.useMutation(); + const { mutateAsync: updateLocal, isPending: isLocalPending } = + api.settings.updateLogManagement.useMutation(); + + const isPending = serverId ? isServerPending : isLocalPending; + const enabled = serverId + ? !!server?.enableLogManagement + : !!webServerSettings?.enableLogManagement; + + const handleToggle = async (checked: boolean) => { + if (serverId) { + await updateServer({ serverId, enableLogManagement: checked }) + .then((result) => { + utils.server.one.setData({ serverId }, (old) => + old + ? { ...old, enableLogManagement: result.enableLogManagement } + : old, + ); + if (checked && !result.installed) { + toast.message( + "Log Management enabled, but no log provider is configured yet — add one under Settings > Log Management.", + ); + } else { + toast.success("Log Management updated"); + } + }) + .catch((e) => { + toast.error( + e instanceof Error ? e.message : "Error updating Log Management", + ); + }); + return; + } + + await updateLocal({ enableLogManagement: checked }) + .then((result) => { + utils.settings.getWebServerSettings.setData(undefined, (old) => + old + ? { ...old, enableLogManagement: result.enableLogManagement } + : old, + ); + if (checked && !result.installed) { + toast.message( + "Log Management enabled, but no log provider is configured yet — add one under Settings > Log Management.", + ); + } else { + toast.success("Log Management updated"); + } + }) + .catch((e) => { + toast.error( + e instanceof Error ? e.message : "Error updating Log Management", + ); + }); + }; + + return ( +
+ + {isPending && ( + + )} + + + + + + +

+ Ships {serverId ? "this server's" : "this machine's"} container + logs to the log providers configured under Settings > Log + Management, via a Vector agent. May take a few seconds to install + or remove. +

+

+ Has no effect (and installs nothing) unless at least one log + provider is enabled for this organization. +

+ {!serverId && ( +

+ Ships every container on this machine, not just this + organization's — if another organization on this instance also + deploys here, its logs ship too (unscoped, no project/app tags). +

+ )} +
+
+
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/settings/servers/delete-server-modal.tsx b/apps/dokploy/components/dashboard/settings/servers/delete-server-modal.tsx index 6f6e72b11..00beb8ea4 100644 --- a/apps/dokploy/components/dashboard/settings/servers/delete-server-modal.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/delete-server-modal.tsx @@ -109,8 +109,13 @@ export const DeleteServerModal = ({ const handleDeleteServer = async () => { try { - await deleteServer({ serverId }); + const result = await deleteServer({ serverId }); toast.success(`Server ${serverName} deleted successfully`); + if (result?.vectorAgentRemovalWarning) { + toast.error( + `Couldn't remove the Vector log agent from this host — it may still be running there: ${result.vectorAgentRemovalWarning}`, + ); + } setOpen(false); utils.server.all.invalidate(); } catch (error: any) { diff --git a/apps/dokploy/components/dashboard/settings/servers/handle-servers.tsx b/apps/dokploy/components/dashboard/settings/servers/handle-servers.tsx index d2c158f91..7dfb62897 100644 --- a/apps/dokploy/components/dashboard/settings/servers/handle-servers.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/handle-servers.tsx @@ -126,10 +126,13 @@ export const HandleServers = ({ serverId, asButton = false }: Props) => { enableDockerCleanup: data.enableDockerCleanup, serverId: serverId || "", }) - .then(async (_data) => { + .then(async (data) => { await utils.server.all.invalidate(); refetchServer(); toast.success(serverId ? "Server Updated" : "Server Created"); + if ("vectorAgentWarning" in data && data.vectorAgentWarning) { + toast.error(data.vectorAgentWarning); + } setIsOpen(false); }) .catch(() => { diff --git a/apps/dokploy/components/dashboard/settings/web-server.tsx b/apps/dokploy/components/dashboard/settings/web-server.tsx index 8238e585b..0f66c1305 100644 --- a/apps/dokploy/components/dashboard/settings/web-server.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server.tsx @@ -13,6 +13,7 @@ import { ShowDokployActions } from "./servers/actions/show-dokploy-actions"; import { ShowStorageActions } from "./servers/actions/show-storage-actions"; import { ShowTraefikActions } from "./servers/actions/show-traefik-actions"; import { ToggleDockerCleanup } from "./servers/actions/toggle-docker-cleanup"; +import { ToggleLogManagement } from "./servers/actions/toggle-log-management"; import { UpdateServer } from "./web-server/update-server"; export const WebServer = () => { @@ -68,6 +69,7 @@ export const WebServer = () => { + diff --git a/apps/dokploy/components/layouts/side.tsx b/apps/dokploy/components/layouts/side.tsx index 1e20a0fbe..2064202c6 100644 --- a/apps/dokploy/components/layouts/side.tsx +++ b/apps/dokploy/components/layouts/side.tsx @@ -29,6 +29,7 @@ import { type LucideIcon, Package, Palette, + ScrollText, Server, ShieldCheck, Smartphone, @@ -374,6 +375,13 @@ const MENU: Menu = { icon: Package, isEnabled: ({ permissions }) => !!permissions?.registry.read, }, + { + isSingle: true, + title: "Log Management", + url: "/dashboard/settings/log-providers", + icon: ScrollText, + isEnabled: ({ permissions }) => !!permissions?.logProvider.read, + }, { isSingle: true, title: "Secrets", diff --git a/apps/dokploy/components/proprietary/audit-logs/columns.tsx b/apps/dokploy/components/proprietary/audit-logs/columns.tsx index 517335e29..eb7aefdc5 100644 --- a/apps/dokploy/components/proprietary/audit-logs/columns.tsx +++ b/apps/dokploy/components/proprietary/audit-logs/columns.tsx @@ -96,6 +96,7 @@ const RESOURCE_LABELS: Record = { settings: "Settings", session: "Session", network: "Network", + logProvider: "Log Provider", }; function MetadataCell({ metadata }: { metadata: string | null }) { diff --git a/apps/dokploy/components/proprietary/roles/manage-custom-roles.tsx b/apps/dokploy/components/proprietary/roles/manage-custom-roles.tsx index 73409dc1a..f09328f8c 100644 --- a/apps/dokploy/components/proprietary/roles/manage-custom-roles.tsx +++ b/apps/dokploy/components/proprietary/roles/manage-custom-roles.tsx @@ -180,6 +180,11 @@ const RESOURCE_META: Record = { description: "Manage DNS providers (Cloudflare, AWS Route53) and create, update, or delete their DNS records", }, + logProvider: { + label: "Log Providers", + description: + "Manage log management providers (Grafana Loki, Datadog, Better Stack) used to ship container logs", + }, }; /** Descriptions for each action within a resource */ @@ -475,6 +480,14 @@ const ACTION_META: Record< description: "Remove DNS providers and delete their records", }, }, + logProvider: { + read: { label: "Read", description: "View configured log providers" }, + create: { + label: "Create", + description: "Add new log providers and test their connection", + }, + delete: { label: "Delete", description: "Remove log providers" }, + }, }; /** Resources that should be hidden from the custom role editor (better-auth internals) */ diff --git a/apps/dokploy/drizzle/0197_past_omega_red.sql b/apps/dokploy/drizzle/0197_past_omega_red.sql new file mode 100644 index 000000000..050b82d13 --- /dev/null +++ b/apps/dokploy/drizzle/0197_past_omega_red.sql @@ -0,0 +1,19 @@ +CREATE TYPE "public"."LogProviderType" AS ENUM('loki', 'datadog', 'betterstack', 'elasticsearch', 'splunk_hec', 'aws_cloudwatch');--> statement-breakpoint +CREATE TABLE "logProvider" ( + "logProviderId" text PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "providerType" "LogProviderType" NOT NULL, + "endpoint" text, + "apiKey" text, + "apiSecret" text, + "extraConfig" jsonb, + "enabled" boolean DEFAULT true NOT NULL, + "createdAt" text NOT NULL, + "organizationId" text NOT NULL +); +--> statement-breakpoint +ALTER TABLE "server" ADD COLUMN "enableLogManagement" boolean DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE "webServerSettings" ADD COLUMN "enableLogManagement" boolean DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE "webServerSettings" ADD COLUMN "logManagementOrganizationId" text;--> statement-breakpoint +ALTER TABLE "logProvider" ADD CONSTRAINT "logProvider_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "webServerSettings" ADD CONSTRAINT "webServerSettings_logManagementOrganizationId_organization_id_fk" FOREIGN KEY ("logManagementOrganizationId") REFERENCES "public"."organization"("id") ON DELETE set null ON UPDATE no action; \ No newline at end of file diff --git a/apps/dokploy/drizzle/meta/0197_snapshot.json b/apps/dokploy/drizzle/meta/0197_snapshot.json new file mode 100644 index 000000000..977f331b2 --- /dev/null +++ b/apps/dokploy/drizzle/meta/0197_snapshot.json @@ -0,0 +1,9305 @@ +{ + "id": "04d812fc-56cf-48bf-92a4-8afdf426b380", + "prevId": "c9ad226a-7a8f-4c3b-8f04-fef6bccf9bfb", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is2FAEnabled": { + "name": "is2FAEnabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "resetPasswordToken": { + "name": "resetPasswordToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resetPasswordExpiresAt": { + "name": "resetPasswordExpiresAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmationToken": { + "name": "confirmationToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmationExpiresAt": { + "name": "confirmationExpiresAt", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apikey": { + "name": "apikey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "apikey_reference_id_user_id_fk": { + "name": "apikey_reference_id_user_id_fk", + "tableFrom": "apikey", + "tableTo": "user", + "columnsFrom": [ + "reference_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canCreateProjects": { + "name": "canCreateProjects", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToSSHKeys": { + "name": "canAccessToSSHKeys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canCreateServices": { + "name": "canCreateServices", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canDeleteProjects": { + "name": "canDeleteProjects", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canDeleteServices": { + "name": "canDeleteServices", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToDocker": { + "name": "canAccessToDocker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToAPI": { + "name": "canAccessToAPI", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToGitProviders": { + "name": "canAccessToGitProviders", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToTraefikFiles": { + "name": "canAccessToTraefikFiles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canDeleteEnvironments": { + "name": "canDeleteEnvironments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canCreateEnvironments": { + "name": "canCreateEnvironments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "accesedProjects": { + "name": "accesedProjects", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "accessedEnvironments": { + "name": "accessedEnvironments", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "accesedServices": { + "name": "accesedServices", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "accessedGitProviders": { + "name": "accessedGitProviders", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "accessedServers": { + "name": "accessedServers", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + } + }, + "indexes": {}, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_role": { + "name": "default_role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "organization_owner_id_user_id_fk": { + "name": "organization_owner_id_user_id_fk", + "tableFrom": "organization", + "tableTo": "user", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_role": { + "name": "organization_role", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "organizationRole_organizationId_idx": { + "name": "organizationRole_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "organizationRole_role_idx": { + "name": "organizationRole_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_role_organization_id_organization_id_fk": { + "name": "organization_role_organization_id_organization_id_fk", + "tableFrom": "organization_role", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.passkey": { + "name": "passkey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "aaguid": { + "name": "aaguid", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "passkey_userId_idx": { + "name": "passkey_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "passkey_credentialID_idx": { + "name": "passkey_credentialID_idx", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "passkey_user_id_user_id_fk": { + "name": "passkey_user_id_user_id_fk", + "tableFrom": "passkey", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.two_factor": { + "name": "two_factor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backup_codes": { + "name": "backup_codes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "failed_verification_count": { + "name": "failed_verification_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "locked_until": { + "name": "locked_until", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "two_factor_user_id_user_id_fk": { + "name": "two_factor_user_id_user_id_fk", + "tableFrom": "two_factor", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai": { + "name": "ai", + "schema": "", + "columns": { + "aiId": { + "name": "aiId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "apiUrl": { + "name": "apiUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "apiKey": { + "name": "apiKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "isEnabled": { + "name": "isEnabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "ai_organizationId_organization_id_fk": { + "name": "ai_organizationId_organization_id_fk", + "tableFrom": "ai", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.application": { + "name": "application", + "schema": "", + "columns": { + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewEnv": { + "name": "previewEnv", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "watchPaths": { + "name": "watchPaths", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "previewBuildArgs": { + "name": "previewBuildArgs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewBuildSecrets": { + "name": "previewBuildSecrets", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewLabels": { + "name": "previewLabels", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "previewWildcard": { + "name": "previewWildcard", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewPort": { + "name": "previewPort", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3000 + }, + "previewHttps": { + "name": "previewHttps", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "previewPath": { + "name": "previewPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "previewCustomCertResolver": { + "name": "previewCustomCertResolver", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewLimit": { + "name": "previewLimit", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "isPreviewDeploymentsActive": { + "name": "isPreviewDeploymentsActive", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "previewRequireCollaboratorPermissions": { + "name": "previewRequireCollaboratorPermissions", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rollbackActive": { + "name": "rollbackActive", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "buildArgs": { + "name": "buildArgs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildSecrets": { + "name": "buildSecrets", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "subtitle": { + "name": "subtitle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "args": { + "name": "args", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sourceType": { + "name": "sourceType", + "type": "sourceType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "cleanCache": { + "name": "cleanCache", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "repository": { + "name": "repository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildPath": { + "name": "buildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "triggerType": { + "name": "triggerType", + "type": "triggerType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'push'" + }, + "autoDeploy": { + "name": "autoDeploy", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "gitlabProjectId": { + "name": "gitlabProjectId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "gitlabRepository": { + "name": "gitlabRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabOwner": { + "name": "gitlabOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabBranch": { + "name": "gitlabBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabBuildPath": { + "name": "gitlabBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "gitlabPathNamespace": { + "name": "gitlabPathNamespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaRepository": { + "name": "giteaRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaOwner": { + "name": "giteaOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaBranch": { + "name": "giteaBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaBuildPath": { + "name": "giteaBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "bitbucketRepository": { + "name": "bitbucketRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketRepositorySlug": { + "name": "bitbucketRepositorySlug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketOwner": { + "name": "bitbucketOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketBranch": { + "name": "bitbucketBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketBuildPath": { + "name": "bitbucketBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registryUrl": { + "name": "registryUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitUrl": { + "name": "customGitUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitBranch": { + "name": "customGitBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitBuildPath": { + "name": "customGitBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitSSHKeyId": { + "name": "customGitSSHKeyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enableSubmodules": { + "name": "enableSubmodules", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dockerfile": { + "name": "dockerfile", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'Dockerfile'" + }, + "dockerContextPath": { + "name": "dockerContextPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerBuildStage": { + "name": "dockerBuildStage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dropBuildPath": { + "name": "dropBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "ulimitsSwarm": { + "name": "ulimitsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "buildType": { + "name": "buildType", + "type": "buildType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'nixpacks'" + }, + "railpackVersion": { + "name": "railpackVersion", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'0.15.4'" + }, + "herokuVersion": { + "name": "herokuVersion", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'24'" + }, + "publishDirectory": { + "name": "publishDirectory", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isStaticSpa": { + "name": "isStaticSpa", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "createEnvFile": { + "name": "createEnvFile", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "registryId": { + "name": "registryId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rollbackRegistryId": { + "name": "rollbackRegistryId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "githubId": { + "name": "githubId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabId": { + "name": "gitlabId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaId": { + "name": "giteaId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketId": { + "name": "bitbucketId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildServerId": { + "name": "buildServerId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildRegistryId": { + "name": "buildRegistryId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "networkIds": { + "name": "networkIds", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "detachDokployNetwork": { + "name": "detachDokployNetwork", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "application_customGitSSHKeyId_ssh-key_sshKeyId_fk": { + "name": "application_customGitSSHKeyId_ssh-key_sshKeyId_fk", + "tableFrom": "application", + "tableTo": "ssh-key", + "columnsFrom": [ + "customGitSSHKeyId" + ], + "columnsTo": [ + "sshKeyId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_registryId_registry_registryId_fk": { + "name": "application_registryId_registry_registryId_fk", + "tableFrom": "application", + "tableTo": "registry", + "columnsFrom": [ + "registryId" + ], + "columnsTo": [ + "registryId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_rollbackRegistryId_registry_registryId_fk": { + "name": "application_rollbackRegistryId_registry_registryId_fk", + "tableFrom": "application", + "tableTo": "registry", + "columnsFrom": [ + "rollbackRegistryId" + ], + "columnsTo": [ + "registryId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_environmentId_environment_environmentId_fk": { + "name": "application_environmentId_environment_environmentId_fk", + "tableFrom": "application", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "application_githubId_github_githubId_fk": { + "name": "application_githubId_github_githubId_fk", + "tableFrom": "application", + "tableTo": "github", + "columnsFrom": [ + "githubId" + ], + "columnsTo": [ + "githubId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_gitlabId_gitlab_gitlabId_fk": { + "name": "application_gitlabId_gitlab_gitlabId_fk", + "tableFrom": "application", + "tableTo": "gitlab", + "columnsFrom": [ + "gitlabId" + ], + "columnsTo": [ + "gitlabId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_giteaId_gitea_giteaId_fk": { + "name": "application_giteaId_gitea_giteaId_fk", + "tableFrom": "application", + "tableTo": "gitea", + "columnsFrom": [ + "giteaId" + ], + "columnsTo": [ + "giteaId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_bitbucketId_bitbucket_bitbucketId_fk": { + "name": "application_bitbucketId_bitbucket_bitbucketId_fk", + "tableFrom": "application", + "tableTo": "bitbucket", + "columnsFrom": [ + "bitbucketId" + ], + "columnsTo": [ + "bitbucketId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_serverId_server_serverId_fk": { + "name": "application_serverId_server_serverId_fk", + "tableFrom": "application", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "application_buildServerId_server_serverId_fk": { + "name": "application_buildServerId_server_serverId_fk", + "tableFrom": "application", + "tableTo": "server", + "columnsFrom": [ + "buildServerId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_buildRegistryId_registry_registryId_fk": { + "name": "application_buildRegistryId_registry_registryId_fk", + "tableFrom": "application", + "tableTo": "registry", + "columnsFrom": [ + "buildRegistryId" + ], + "columnsTo": [ + "registryId" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "application_appName_unique": { + "name": "application_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_email": { + "name": "user_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_role": { + "name": "user_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "auditLog_organizationId_idx": { + "name": "auditLog_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auditLog_userId_idx": { + "name": "auditLog_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auditLog_createdAt_idx": { + "name": "auditLog_createdAt_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_log_organization_id_organization_id_fk": { + "name": "audit_log_organization_id_organization_id_fk", + "tableFrom": "audit_log", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_log_user_id_user_id_fk": { + "name": "audit_log_user_id_user_id_fk", + "tableFrom": "audit_log", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.backup": { + "name": "backup", + "schema": "", + "columns": { + "backupId": { + "name": "backupId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule": { + "name": "schedule", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "database": { + "name": "database", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serviceName": { + "name": "serviceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "destinationId": { + "name": "destinationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "keepLatestCount": { + "name": "keepLatestCount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "includeEncryptionKey": { + "name": "includeEncryptionKey", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "backupType": { + "name": "backupType", + "type": "backupType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'database'" + }, + "databaseType": { + "name": "databaseType", + "type": "databaseType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "libsqlId": { + "name": "libsqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "backup_destinationId_destination_destinationId_fk": { + "name": "backup_destinationId_destination_destinationId_fk", + "tableFrom": "backup", + "tableTo": "destination", + "columnsFrom": [ + "destinationId" + ], + "columnsTo": [ + "destinationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_composeId_compose_composeId_fk": { + "name": "backup_composeId_compose_composeId_fk", + "tableFrom": "backup", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_postgresId_postgres_postgresId_fk": { + "name": "backup_postgresId_postgres_postgresId_fk", + "tableFrom": "backup", + "tableTo": "postgres", + "columnsFrom": [ + "postgresId" + ], + "columnsTo": [ + "postgresId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_mariadbId_mariadb_mariadbId_fk": { + "name": "backup_mariadbId_mariadb_mariadbId_fk", + "tableFrom": "backup", + "tableTo": "mariadb", + "columnsFrom": [ + "mariadbId" + ], + "columnsTo": [ + "mariadbId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_mysqlId_mysql_mysqlId_fk": { + "name": "backup_mysqlId_mysql_mysqlId_fk", + "tableFrom": "backup", + "tableTo": "mysql", + "columnsFrom": [ + "mysqlId" + ], + "columnsTo": [ + "mysqlId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_mongoId_mongo_mongoId_fk": { + "name": "backup_mongoId_mongo_mongoId_fk", + "tableFrom": "backup", + "tableTo": "mongo", + "columnsFrom": [ + "mongoId" + ], + "columnsTo": [ + "mongoId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_libsqlId_libsql_libsqlId_fk": { + "name": "backup_libsqlId_libsql_libsqlId_fk", + "tableFrom": "backup", + "tableTo": "libsql", + "columnsFrom": [ + "libsqlId" + ], + "columnsTo": [ + "libsqlId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_userId_user_id_fk": { + "name": "backup_userId_user_id_fk", + "tableFrom": "backup", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "backup_appName_unique": { + "name": "backup_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bitbucket": { + "name": "bitbucket", + "schema": "", + "columns": { + "bitbucketId": { + "name": "bitbucketId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "bitbucketUsername": { + "name": "bitbucketUsername", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketEmail": { + "name": "bitbucketEmail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "appPassword": { + "name": "appPassword", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "apiToken": { + "name": "apiToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketWorkspaceName": { + "name": "bitbucketWorkspaceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "bitbucket_gitProviderId_git_provider_gitProviderId_fk": { + "name": "bitbucket_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "bitbucket", + "tableTo": "git_provider", + "columnsFrom": [ + "gitProviderId" + ], + "columnsTo": [ + "gitProviderId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.certificate": { + "name": "certificate", + "schema": "", + "columns": { + "certificateId": { + "name": "certificateId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "certificateData": { + "name": "certificateData", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "privateKey": { + "name": "privateKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "certificatePath": { + "name": "certificatePath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "autoRenew": { + "name": "autoRenew", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "certificate_organizationId_organization_id_fk": { + "name": "certificate_organizationId_organization_id_fk", + "tableFrom": "certificate", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "certificate_serverId_server_serverId_fk": { + "name": "certificate_serverId_server_serverId_fk", + "tableFrom": "certificate", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "certificate_certificatePath_unique": { + "name": "certificate_certificatePath_unique", + "nullsNotDistinct": false, + "columns": [ + "certificatePath" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.compose": { + "name": "compose", + "schema": "", + "columns": { + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeFile": { + "name": "composeFile", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sourceType": { + "name": "sourceType", + "type": "sourceTypeCompose", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "composeType": { + "name": "composeType", + "type": "composeType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'docker-compose'" + }, + "repository": { + "name": "repository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autoDeploy": { + "name": "autoDeploy", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "gitlabProjectId": { + "name": "gitlabProjectId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "gitlabRepository": { + "name": "gitlabRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabOwner": { + "name": "gitlabOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabBranch": { + "name": "gitlabBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabPathNamespace": { + "name": "gitlabPathNamespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketRepository": { + "name": "bitbucketRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketRepositorySlug": { + "name": "bitbucketRepositorySlug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketOwner": { + "name": "bitbucketOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketBranch": { + "name": "bitbucketBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaRepository": { + "name": "giteaRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaOwner": { + "name": "giteaOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaBranch": { + "name": "giteaBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitUrl": { + "name": "customGitUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitBranch": { + "name": "customGitBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitSSHKeyId": { + "name": "customGitSSHKeyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "createEnvFile": { + "name": "createEnvFile", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enableSubmodules": { + "name": "enableSubmodules", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "composePath": { + "name": "composePath", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'./docker-compose.yml'" + }, + "suffix": { + "name": "suffix", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "randomize": { + "name": "randomize", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "isolatedDeployment": { + "name": "isolatedDeployment", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "isolatedDeploymentsVolume": { + "name": "isolatedDeploymentsVolume", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "pullImages": { + "name": "pullImages", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "triggerType": { + "name": "triggerType", + "type": "triggerType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'push'" + }, + "composeStatus": { + "name": "composeStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "watchPaths": { + "name": "watchPaths", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "githubId": { + "name": "githubId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabId": { + "name": "gitlabId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketId": { + "name": "bitbucketId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaId": { + "name": "giteaId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serviceNetworks": { + "name": "serviceNetworks", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "compose_customGitSSHKeyId_ssh-key_sshKeyId_fk": { + "name": "compose_customGitSSHKeyId_ssh-key_sshKeyId_fk", + "tableFrom": "compose", + "tableTo": "ssh-key", + "columnsFrom": [ + "customGitSSHKeyId" + ], + "columnsTo": [ + "sshKeyId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_environmentId_environment_environmentId_fk": { + "name": "compose_environmentId_environment_environmentId_fk", + "tableFrom": "compose", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "compose_githubId_github_githubId_fk": { + "name": "compose_githubId_github_githubId_fk", + "tableFrom": "compose", + "tableTo": "github", + "columnsFrom": [ + "githubId" + ], + "columnsTo": [ + "githubId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_gitlabId_gitlab_gitlabId_fk": { + "name": "compose_gitlabId_gitlab_gitlabId_fk", + "tableFrom": "compose", + "tableTo": "gitlab", + "columnsFrom": [ + "gitlabId" + ], + "columnsTo": [ + "gitlabId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_bitbucketId_bitbucket_bitbucketId_fk": { + "name": "compose_bitbucketId_bitbucket_bitbucketId_fk", + "tableFrom": "compose", + "tableTo": "bitbucket", + "columnsFrom": [ + "bitbucketId" + ], + "columnsTo": [ + "bitbucketId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_giteaId_gitea_giteaId_fk": { + "name": "compose_giteaId_gitea_giteaId_fk", + "tableFrom": "compose", + "tableTo": "gitea", + "columnsFrom": [ + "giteaId" + ], + "columnsTo": [ + "giteaId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_serverId_server_serverId_fk": { + "name": "compose_serverId_server_serverId_fk", + "tableFrom": "compose", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment": { + "name": "deployment", + "schema": "", + "columns": { + "deploymentId": { + "name": "deploymentId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "deploymentStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'running'" + }, + "logPath": { + "name": "logPath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pid": { + "name": "pid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isPreviewDeployment": { + "name": "isPreviewDeployment", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "previewDeploymentId": { + "name": "previewDeploymentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "startedAt": { + "name": "startedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finishedAt": { + "name": "finishedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "errorMessage": { + "name": "errorMessage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduleId": { + "name": "scheduleId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backupId": { + "name": "backupId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rollbackId": { + "name": "rollbackId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "volumeBackupId": { + "name": "volumeBackupId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildServerId": { + "name": "buildServerId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "deployment_applicationId_application_applicationId_fk": { + "name": "deployment_applicationId_application_applicationId_fk", + "tableFrom": "deployment", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_composeId_compose_composeId_fk": { + "name": "deployment_composeId_compose_composeId_fk", + "tableFrom": "deployment", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_serverId_server_serverId_fk": { + "name": "deployment_serverId_server_serverId_fk", + "tableFrom": "deployment", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_previewDeploymentId_preview_deployments_previewDeploymentId_fk": { + "name": "deployment_previewDeploymentId_preview_deployments_previewDeploymentId_fk", + "tableFrom": "deployment", + "tableTo": "preview_deployments", + "columnsFrom": [ + "previewDeploymentId" + ], + "columnsTo": [ + "previewDeploymentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_scheduleId_schedule_scheduleId_fk": { + "name": "deployment_scheduleId_schedule_scheduleId_fk", + "tableFrom": "deployment", + "tableTo": "schedule", + "columnsFrom": [ + "scheduleId" + ], + "columnsTo": [ + "scheduleId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_backupId_backup_backupId_fk": { + "name": "deployment_backupId_backup_backupId_fk", + "tableFrom": "deployment", + "tableTo": "backup", + "columnsFrom": [ + "backupId" + ], + "columnsTo": [ + "backupId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_rollbackId_rollback_rollbackId_fk": { + "name": "deployment_rollbackId_rollback_rollbackId_fk", + "tableFrom": "deployment", + "tableTo": "rollback", + "columnsFrom": [ + "rollbackId" + ], + "columnsTo": [ + "rollbackId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_volumeBackupId_volume_backup_volumeBackupId_fk": { + "name": "deployment_volumeBackupId_volume_backup_volumeBackupId_fk", + "tableFrom": "deployment", + "tableTo": "volume_backup", + "columnsFrom": [ + "volumeBackupId" + ], + "columnsTo": [ + "volumeBackupId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_buildServerId_server_serverId_fk": { + "name": "deployment_buildServerId_server_serverId_fk", + "tableFrom": "deployment", + "tableTo": "server", + "columnsFrom": [ + "buildServerId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.destination": { + "name": "destination", + "schema": "", + "columns": { + "destinationId": { + "name": "destinationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessKey": { + "name": "accessKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secretAccessKey": { + "name": "secretAccessKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bucket": { + "name": "bucket", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "additionalFlags": { + "name": "additionalFlags", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "destination_organizationId_organization_id_fk": { + "name": "destination_organizationId_organization_id_fk", + "tableFrom": "destination", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dns_provider": { + "name": "dns_provider", + "schema": "", + "columns": { + "dnsProviderId": { + "name": "dnsProviderId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerType": { + "name": "providerType", + "type": "DnsProviderType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "dns_provider_org_name_idx": { + "name": "dns_provider_org_name_idx", + "columns": [ + { + "expression": "organizationId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "dns_provider_organizationId_organization_id_fk": { + "name": "dns_provider_organizationId_organization_id_fk", + "tableFrom": "dns_provider", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.domain": { + "name": "domain", + "schema": "", + "columns": { + "domainId": { + "name": "domainId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "https": { + "name": "https", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3000 + }, + "customEntrypoint": { + "name": "customEntrypoint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "serviceName": { + "name": "serviceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domainType": { + "name": "domainType", + "type": "domainType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'application'" + }, + "uniqueConfigKey": { + "name": "uniqueConfigKey", + "type": "serial", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customCertResolver": { + "name": "customCertResolver", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewDeploymentId": { + "name": "previewDeploymentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "internalPath": { + "name": "internalPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "stripPath": { + "name": "stripPath", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "middlewares": { + "name": "middlewares", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "ARRAY[]::text[]" + }, + "forwardAuthEnabled": { + "name": "forwardAuthEnabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "domain_composeId_compose_composeId_fk": { + "name": "domain_composeId_compose_composeId_fk", + "tableFrom": "domain", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "domain_applicationId_application_applicationId_fk": { + "name": "domain_applicationId_application_applicationId_fk", + "tableFrom": "domain", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "domain_previewDeploymentId_preview_deployments_previewDeploymentId_fk": { + "name": "domain_previewDeploymentId_preview_deployments_previewDeploymentId_fk", + "tableFrom": "domain", + "tableTo": "preview_deployments", + "columnsFrom": [ + "previewDeploymentId" + ], + "columnsTo": [ + "previewDeploymentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "isDefault": { + "name": "isDefault", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "environment_projectId_project_projectId_fk": { + "name": "environment_projectId_project_projectId_fk", + "tableFrom": "environment", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.forward_auth_settings": { + "name": "forward_auth_settings", + "schema": "", + "columns": { + "forwardAuthSettingsId": { + "name": "forwardAuthSettingsId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "authDomain": { + "name": "authDomain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "baseDomain": { + "name": "baseDomain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "https": { + "name": "https", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'letsencrypt'" + }, + "customCertResolver": { + "name": "customCertResolver", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "providerId": { + "name": "providerId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "forward_auth_settings_providerId_sso_provider_provider_id_fk": { + "name": "forward_auth_settings_providerId_sso_provider_provider_id_fk", + "tableFrom": "forward_auth_settings", + "tableTo": "sso_provider", + "columnsFrom": [ + "providerId" + ], + "columnsTo": [ + "provider_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "forward_auth_settings_serverId_server_serverId_fk": { + "name": "forward_auth_settings_serverId_server_serverId_fk", + "tableFrom": "forward_auth_settings", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "forward_auth_settings_serverId_unique": { + "name": "forward_auth_settings_serverId_unique", + "nullsNotDistinct": false, + "columns": [ + "serverId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_provider": { + "name": "git_provider", + "schema": "", + "columns": { + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerType": { + "name": "providerType", + "type": "gitProviderType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sharedWithOrganization": { + "name": "sharedWithOrganization", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "git_provider_organizationId_organization_id_fk": { + "name": "git_provider_organizationId_organization_id_fk", + "tableFrom": "git_provider", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "git_provider_userId_user_id_fk": { + "name": "git_provider_userId_user_id_fk", + "tableFrom": "git_provider", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.gitea": { + "name": "gitea", + "schema": "", + "columns": { + "giteaId": { + "name": "giteaId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "giteaUrl": { + "name": "giteaUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'https://gitea.com'" + }, + "giteaInternalUrl": { + "name": "giteaInternalUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'repo,repo:status,read:user,read:org'" + }, + "last_authenticated_at": { + "name": "last_authenticated_at", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "gitea_gitProviderId_git_provider_gitProviderId_fk": { + "name": "gitea_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "gitea", + "tableTo": "git_provider", + "columnsFrom": [ + "gitProviderId" + ], + "columnsTo": [ + "gitProviderId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.github": { + "name": "github", + "schema": "", + "columns": { + "githubId": { + "name": "githubId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "githubAppName": { + "name": "githubAppName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubAppId": { + "name": "githubAppId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "githubClientId": { + "name": "githubClientId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubClientSecret": { + "name": "githubClientSecret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubInstallationId": { + "name": "githubInstallationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubPrivateKey": { + "name": "githubPrivateKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubWebhookSecret": { + "name": "githubWebhookSecret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubUrl": { + "name": "githubUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'https://github.com'" + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "github_gitProviderId_git_provider_gitProviderId_fk": { + "name": "github_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "github", + "tableTo": "git_provider", + "columnsFrom": [ + "gitProviderId" + ], + "columnsTo": [ + "gitProviderId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.gitlab": { + "name": "gitlab", + "schema": "", + "columns": { + "gitlabId": { + "name": "gitlabId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "gitlabUrl": { + "name": "gitlabUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'https://gitlab.com'" + }, + "gitlabInternalUrl": { + "name": "gitlabInternalUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "application_id": { + "name": "application_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "group_name": { + "name": "group_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "gitlab_gitProviderId_git_provider_gitProviderId_fk": { + "name": "gitlab_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "gitlab", + "tableTo": "git_provider", + "columnsFrom": [ + "gitProviderId" + ], + "columnsTo": [ + "gitProviderId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.libsql": { + "name": "libsql", + "schema": "", + "columns": { + "libsqlId": { + "name": "libsqlId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sqldNode": { + "name": "sqldNode", + "type": "sqldNode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'primary'" + }, + "sqldPrimaryUrl": { + "name": "sqldPrimaryUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enableNamespaces": { + "name": "enableNamespaces", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "externalGRPCPort": { + "name": "externalGRPCPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "externalAdminPort": { + "name": "externalAdminPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "networkIds": { + "name": "networkIds", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "detachDokployNetwork": { + "name": "detachDokployNetwork", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "libsql_environmentId_environment_environmentId_fk": { + "name": "libsql_environmentId_environment_environmentId_fk", + "tableFrom": "libsql", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "libsql_serverId_server_serverId_fk": { + "name": "libsql_serverId_server_serverId_fk", + "tableFrom": "libsql", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "libsql_appName_unique": { + "name": "libsql_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.logProvider": { + "name": "logProvider", + "schema": "", + "columns": { + "logProviderId": { + "name": "logProviderId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerType": { + "name": "providerType", + "type": "LogProviderType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "apiKey": { + "name": "apiKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "apiSecret": { + "name": "apiSecret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "extraConfig": { + "name": "extraConfig", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "logProvider_organizationId_organization_id_fk": { + "name": "logProvider_organizationId_organization_id_fk", + "tableFrom": "logProvider", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mariadb": { + "name": "mariadb", + "schema": "", + "columns": { + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseName": { + "name": "databaseName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rootPassword": { + "name": "rootPassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "args": { + "name": "args", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "ulimitsSwarm": { + "name": "ulimitsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "networkIds": { + "name": "networkIds", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "detachDokployNetwork": { + "name": "detachDokployNetwork", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "mariadb_environmentId_environment_environmentId_fk": { + "name": "mariadb_environmentId_environment_environmentId_fk", + "tableFrom": "mariadb", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mariadb_serverId_server_serverId_fk": { + "name": "mariadb_serverId_server_serverId_fk", + "tableFrom": "mariadb", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mariadb_appName_unique": { + "name": "mariadb_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mongo": { + "name": "mongo", + "schema": "", + "columns": { + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'mongo:8'" + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "args": { + "name": "args", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "ulimitsSwarm": { + "name": "ulimitsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "replicaSets": { + "name": "replicaSets", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "networkIds": { + "name": "networkIds", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "detachDokployNetwork": { + "name": "detachDokployNetwork", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "mongo_environmentId_environment_environmentId_fk": { + "name": "mongo_environmentId_environment_environmentId_fk", + "tableFrom": "mongo", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mongo_serverId_server_serverId_fk": { + "name": "mongo_serverId_server_serverId_fk", + "tableFrom": "mongo", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mongo_appName_unique": { + "name": "mongo_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mount": { + "name": "mount", + "schema": "", + "columns": { + "mountId": { + "name": "mountId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "mountType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "hostPath": { + "name": "hostPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "volumeName": { + "name": "volumeName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "filePath": { + "name": "filePath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serviceType": { + "name": "serviceType", + "type": "serviceType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'application'" + }, + "mountPath": { + "name": "mountPath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "libsqlId": { + "name": "libsqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redisId": { + "name": "redisId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "mount_applicationId_application_applicationId_fk": { + "name": "mount_applicationId_application_applicationId_fk", + "tableFrom": "mount", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_composeId_compose_composeId_fk": { + "name": "mount_composeId_compose_composeId_fk", + "tableFrom": "mount", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_libsqlId_libsql_libsqlId_fk": { + "name": "mount_libsqlId_libsql_libsqlId_fk", + "tableFrom": "mount", + "tableTo": "libsql", + "columnsFrom": [ + "libsqlId" + ], + "columnsTo": [ + "libsqlId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_mariadbId_mariadb_mariadbId_fk": { + "name": "mount_mariadbId_mariadb_mariadbId_fk", + "tableFrom": "mount", + "tableTo": "mariadb", + "columnsFrom": [ + "mariadbId" + ], + "columnsTo": [ + "mariadbId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_mongoId_mongo_mongoId_fk": { + "name": "mount_mongoId_mongo_mongoId_fk", + "tableFrom": "mount", + "tableTo": "mongo", + "columnsFrom": [ + "mongoId" + ], + "columnsTo": [ + "mongoId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_mysqlId_mysql_mysqlId_fk": { + "name": "mount_mysqlId_mysql_mysqlId_fk", + "tableFrom": "mount", + "tableTo": "mysql", + "columnsFrom": [ + "mysqlId" + ], + "columnsTo": [ + "mysqlId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_postgresId_postgres_postgresId_fk": { + "name": "mount_postgresId_postgres_postgresId_fk", + "tableFrom": "mount", + "tableTo": "postgres", + "columnsFrom": [ + "postgresId" + ], + "columnsTo": [ + "postgresId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_redisId_redis_redisId_fk": { + "name": "mount_redisId_redis_redisId_fk", + "tableFrom": "mount", + "tableTo": "redis", + "columnsFrom": [ + "redisId" + ], + "columnsTo": [ + "redisId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mysql": { + "name": "mysql", + "schema": "", + "columns": { + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseName": { + "name": "databaseName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rootPassword": { + "name": "rootPassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "args": { + "name": "args", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "ulimitsSwarm": { + "name": "ulimitsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "networkIds": { + "name": "networkIds", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "detachDokployNetwork": { + "name": "detachDokployNetwork", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "mysql_environmentId_environment_environmentId_fk": { + "name": "mysql_environmentId_environment_environmentId_fk", + "tableFrom": "mysql", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mysql_serverId_server_serverId_fk": { + "name": "mysql_serverId_server_serverId_fk", + "tableFrom": "mysql", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mysql_appName_unique": { + "name": "mysql_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.network": { + "name": "network", + "schema": "", + "columns": { + "networkId": { + "name": "networkId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerId": { + "name": "dockerId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "driver": { + "name": "driver", + "type": "networkDriver", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'bridge'" + }, + "internal": { + "name": "internal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "attachable": { + "name": "attachable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enableIPv4": { + "name": "enableIPv4", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enableIPv6": { + "name": "enableIPv6", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "mtu": { + "name": "mtu", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ipam": { + "name": "ipam", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "network_organizationId_organization_id_fk": { + "name": "network_organizationId_organization_id_fk", + "tableFrom": "network", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "network_serverId_server_serverId_fk": { + "name": "network_serverId_server_serverId_fk", + "tableFrom": "network", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom": { + "name": "custom", + "schema": "", + "columns": { + "customId": { + "name": "customId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "headers": { + "name": "headers", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.discord": { + "name": "discord", + "schema": "", + "columns": { + "discordId": { + "name": "discordId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "decoration": { + "name": "decoration", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email": { + "name": "email", + "schema": "", + "columns": { + "emailId": { + "name": "emailId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "smtpServer": { + "name": "smtpServer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "smtpPort": { + "name": "smtpPort", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fromAddress": { + "name": "fromAddress", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "toAddress": { + "name": "toAddress", + "type": "text[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.gotify": { + "name": "gotify", + "schema": "", + "columns": { + "gotifyId": { + "name": "gotifyId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "serverUrl": { + "name": "serverUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appToken": { + "name": "appToken", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "decoration": { + "name": "decoration", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.lark": { + "name": "lark", + "schema": "", + "columns": { + "larkId": { + "name": "larkId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mattermost": { + "name": "mattermost", + "schema": "", + "columns": { + "mattermostId": { + "name": "mattermostId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification": { + "name": "notification", + "schema": "", + "columns": { + "notificationId": { + "name": "notificationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appDeploy": { + "name": "appDeploy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "appBuildError": { + "name": "appBuildError", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "databaseBackup": { + "name": "databaseBackup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "volumeBackup": { + "name": "volumeBackup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dokployRestart": { + "name": "dokployRestart", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dokployBackup": { + "name": "dokployBackup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dockerCleanup": { + "name": "dockerCleanup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "serverThreshold": { + "name": "serverThreshold", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "notificationType": { + "name": "notificationType", + "type": "notificationType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slackId": { + "name": "slackId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telegramId": { + "name": "telegramId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "discordId": { + "name": "discordId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "emailId": { + "name": "emailId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resendId": { + "name": "resendId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gotifyId": { + "name": "gotifyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ntfyId": { + "name": "ntfyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mattermostId": { + "name": "mattermostId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customId": { + "name": "customId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "larkId": { + "name": "larkId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pushoverId": { + "name": "pushoverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "teamsId": { + "name": "teamsId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "notification_slackId_slack_slackId_fk": { + "name": "notification_slackId_slack_slackId_fk", + "tableFrom": "notification", + "tableTo": "slack", + "columnsFrom": [ + "slackId" + ], + "columnsTo": [ + "slackId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_telegramId_telegram_telegramId_fk": { + "name": "notification_telegramId_telegram_telegramId_fk", + "tableFrom": "notification", + "tableTo": "telegram", + "columnsFrom": [ + "telegramId" + ], + "columnsTo": [ + "telegramId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_discordId_discord_discordId_fk": { + "name": "notification_discordId_discord_discordId_fk", + "tableFrom": "notification", + "tableTo": "discord", + "columnsFrom": [ + "discordId" + ], + "columnsTo": [ + "discordId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_emailId_email_emailId_fk": { + "name": "notification_emailId_email_emailId_fk", + "tableFrom": "notification", + "tableTo": "email", + "columnsFrom": [ + "emailId" + ], + "columnsTo": [ + "emailId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_resendId_resend_resendId_fk": { + "name": "notification_resendId_resend_resendId_fk", + "tableFrom": "notification", + "tableTo": "resend", + "columnsFrom": [ + "resendId" + ], + "columnsTo": [ + "resendId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_gotifyId_gotify_gotifyId_fk": { + "name": "notification_gotifyId_gotify_gotifyId_fk", + "tableFrom": "notification", + "tableTo": "gotify", + "columnsFrom": [ + "gotifyId" + ], + "columnsTo": [ + "gotifyId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_ntfyId_ntfy_ntfyId_fk": { + "name": "notification_ntfyId_ntfy_ntfyId_fk", + "tableFrom": "notification", + "tableTo": "ntfy", + "columnsFrom": [ + "ntfyId" + ], + "columnsTo": [ + "ntfyId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_mattermostId_mattermost_mattermostId_fk": { + "name": "notification_mattermostId_mattermost_mattermostId_fk", + "tableFrom": "notification", + "tableTo": "mattermost", + "columnsFrom": [ + "mattermostId" + ], + "columnsTo": [ + "mattermostId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_customId_custom_customId_fk": { + "name": "notification_customId_custom_customId_fk", + "tableFrom": "notification", + "tableTo": "custom", + "columnsFrom": [ + "customId" + ], + "columnsTo": [ + "customId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_larkId_lark_larkId_fk": { + "name": "notification_larkId_lark_larkId_fk", + "tableFrom": "notification", + "tableTo": "lark", + "columnsFrom": [ + "larkId" + ], + "columnsTo": [ + "larkId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_pushoverId_pushover_pushoverId_fk": { + "name": "notification_pushoverId_pushover_pushoverId_fk", + "tableFrom": "notification", + "tableTo": "pushover", + "columnsFrom": [ + "pushoverId" + ], + "columnsTo": [ + "pushoverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_teamsId_teams_teamsId_fk": { + "name": "notification_teamsId_teams_teamsId_fk", + "tableFrom": "notification", + "tableTo": "teams", + "columnsFrom": [ + "teamsId" + ], + "columnsTo": [ + "teamsId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_organizationId_organization_id_fk": { + "name": "notification_organizationId_organization_id_fk", + "tableFrom": "notification", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ntfy": { + "name": "ntfy", + "schema": "", + "columns": { + "ntfyId": { + "name": "ntfyId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "serverUrl": { + "name": "serverUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "topic": { + "name": "topic", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "accessToken": { + "name": "accessToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pushover": { + "name": "pushover", + "schema": "", + "columns": { + "pushoverId": { + "name": "pushoverId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userKey": { + "name": "userKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "apiToken": { + "name": "apiToken", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "retry": { + "name": "retry", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "expire": { + "name": "expire", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resend": { + "name": "resend", + "schema": "", + "columns": { + "resendId": { + "name": "resendId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "apiKey": { + "name": "apiKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fromAddress": { + "name": "fromAddress", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "toAddress": { + "name": "toAddress", + "type": "text[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.slack": { + "name": "slack", + "schema": "", + "columns": { + "slackId": { + "name": "slackId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "teamsId": { + "name": "teamsId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.telegram": { + "name": "telegram", + "schema": "", + "columns": { + "telegramId": { + "name": "telegramId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "botToken": { + "name": "botToken", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chatId": { + "name": "chatId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "messageThreadId": { + "name": "messageThreadId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.patch": { + "name": "patch", + "schema": "", + "columns": { + "patchId": { + "name": "patchId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "patchType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'update'" + }, + "filePath": { + "name": "filePath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "patch_applicationId_application_applicationId_fk": { + "name": "patch_applicationId_application_applicationId_fk", + "tableFrom": "patch", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "patch_composeId_compose_composeId_fk": { + "name": "patch_composeId_compose_composeId_fk", + "tableFrom": "patch", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "patch_filepath_application_unique": { + "name": "patch_filepath_application_unique", + "nullsNotDistinct": false, + "columns": [ + "filePath", + "applicationId" + ] + }, + "patch_filepath_compose_unique": { + "name": "patch_filepath_compose_unique", + "nullsNotDistinct": false, + "columns": [ + "filePath", + "composeId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.port": { + "name": "port", + "schema": "", + "columns": { + "portId": { + "name": "portId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "publishedPort": { + "name": "publishedPort", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "publishMode": { + "name": "publishMode", + "type": "publishModeType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'host'" + }, + "targetPort": { + "name": "targetPort", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "protocolType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "port_applicationId_application_applicationId_fk": { + "name": "port_applicationId_application_applicationId_fk", + "tableFrom": "port", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.postgres": { + "name": "postgres", + "schema": "", + "columns": { + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseName": { + "name": "databaseName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "args": { + "name": "args", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "ulimitsSwarm": { + "name": "ulimitsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "networkIds": { + "name": "networkIds", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "detachDokployNetwork": { + "name": "detachDokployNetwork", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "postgres_environmentId_environment_environmentId_fk": { + "name": "postgres_environmentId_environment_environmentId_fk", + "tableFrom": "postgres", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "postgres_serverId_server_serverId_fk": { + "name": "postgres_serverId_server_serverId_fk", + "tableFrom": "postgres", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "postgres_appName_unique": { + "name": "postgres_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.preview_deployments": { + "name": "preview_deployments", + "schema": "", + "columns": { + "previewDeploymentId": { + "name": "previewDeploymentId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestId": { + "name": "pullRequestId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestNumber": { + "name": "pullRequestNumber", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestURL": { + "name": "pullRequestURL", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestTitle": { + "name": "pullRequestTitle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestCommentId": { + "name": "pullRequestCommentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "previewStatus": { + "name": "previewStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domainId": { + "name": "domainId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "preview_deployments_applicationId_application_applicationId_fk": { + "name": "preview_deployments_applicationId_application_applicationId_fk", + "tableFrom": "preview_deployments", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "preview_deployments_domainId_domain_domainId_fk": { + "name": "preview_deployments_domainId_domain_domainId_fk", + "tableFrom": "preview_deployments", + "tableTo": "domain", + "columnsFrom": [ + "domainId" + ], + "columnsTo": [ + "domainId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "preview_deployments_appName_unique": { + "name": "preview_deployments_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project": { + "name": "project", + "schema": "", + "columns": { + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": { + "project_organizationId_organization_id_fk": { + "name": "project_organizationId_organization_id_fk", + "tableFrom": "project", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.redirect": { + "name": "redirect", + "schema": "", + "columns": { + "redirectId": { + "name": "redirectId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "regex": { + "name": "regex", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "replacement": { + "name": "replacement", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permanent": { + "name": "permanent", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "uniqueConfigKey": { + "name": "uniqueConfigKey", + "type": "serial", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "redirect_applicationId_application_applicationId_fk": { + "name": "redirect_applicationId_application_applicationId_fk", + "tableFrom": "redirect", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.redis": { + "name": "redis", + "schema": "", + "columns": { + "redisId": { + "name": "redisId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "args": { + "name": "args", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "ulimitsSwarm": { + "name": "ulimitsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "networkIds": { + "name": "networkIds", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "detachDokployNetwork": { + "name": "detachDokployNetwork", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "redis_environmentId_environment_environmentId_fk": { + "name": "redis_environmentId_environment_environmentId_fk", + "tableFrom": "redis", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "redis_serverId_server_serverId_fk": { + "name": "redis_serverId_server_serverId_fk", + "tableFrom": "redis", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "redis_appName_unique": { + "name": "redis_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.registry": { + "name": "registry", + "schema": "", + "columns": { + "registryId": { + "name": "registryId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "registryName": { + "name": "registryName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "imagePrefix": { + "name": "imagePrefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "registryUrl": { + "name": "registryUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "selfHosted": { + "name": "selfHosted", + "type": "RegistryType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'cloud'" + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "registry_organizationId_organization_id_fk": { + "name": "registry_organizationId_organization_id_fk", + "tableFrom": "registry", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rollback": { + "name": "rollback", + "schema": "", + "columns": { + "rollbackId": { + "name": "rollbackId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "deploymentId": { + "name": "deploymentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "serial", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fullContext": { + "name": "fullContext", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "rollback_deploymentId_deployment_deploymentId_fk": { + "name": "rollback_deploymentId_deployment_deploymentId_fk", + "tableFrom": "rollback", + "tableTo": "deployment", + "columnsFrom": [ + "deploymentId" + ], + "columnsTo": [ + "deploymentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.schedule": { + "name": "schedule", + "schema": "", + "columns": { + "scheduleId": { + "name": "scheduleId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cronExpression": { + "name": "cronExpression", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serviceName": { + "name": "serviceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shellType": { + "name": "shellType", + "type": "shellType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'bash'" + }, + "scheduleType": { + "name": "scheduleType", + "type": "scheduleType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'application'" + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "script": { + "name": "script", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "schedule_applicationId_application_applicationId_fk": { + "name": "schedule_applicationId_application_applicationId_fk", + "tableFrom": "schedule", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schedule_composeId_compose_composeId_fk": { + "name": "schedule_composeId_compose_composeId_fk", + "tableFrom": "schedule", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schedule_serverId_server_serverId_fk": { + "name": "schedule_serverId_server_serverId_fk", + "tableFrom": "schedule", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schedule_organizationId_organization_id_fk": { + "name": "schedule_organizationId_organization_id_fk", + "tableFrom": "schedule", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_provider": { + "name": "scim_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scim_token": { + "name": "scim_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "scim_provider_organization_id_organization_id_fk": { + "name": "scim_provider_organization_id_organization_id_fk", + "tableFrom": "scim_provider", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_provider_provider_id_unique": { + "name": "scim_provider_provider_id_unique", + "nullsNotDistinct": false, + "columns": [ + "provider_id" + ] + }, + "scim_provider_scim_token_unique": { + "name": "scim_provider_scim_token_unique", + "nullsNotDistinct": false, + "columns": [ + "scim_token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.security": { + "name": "security", + "schema": "", + "columns": { + "securityId": { + "name": "securityId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "security_applicationId_application_applicationId_fk": { + "name": "security_applicationId_application_applicationId_fk", + "tableFrom": "security", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "security_username_applicationId_unique": { + "name": "security_username_applicationId_unique", + "nullsNotDistinct": false, + "columns": [ + "username", + "applicationId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.server": { + "name": "server", + "schema": "", + "columns": { + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ipAddress": { + "name": "ipAddress", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'root'" + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enableDockerCleanup": { + "name": "enableDockerCleanup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enableLogManagement": { + "name": "enableLogManagement", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "buildsConcurrency": { + "name": "buildsConcurrency", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverStatus": { + "name": "serverStatus", + "type": "serverStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "serverType": { + "name": "serverType", + "type": "serverType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'deploy'" + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "sshKeyId": { + "name": "sshKeyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metricsConfig": { + "name": "metricsConfig", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"server\":{\"type\":\"Remote\",\"refreshRate\":60,\"port\":4500,\"token\":\"\",\"urlCallback\":\"\",\"cronJob\":\"\",\"retentionDays\":2,\"thresholds\":{\"cpu\":0,\"memory\":0}},\"containers\":{\"refreshRate\":60,\"services\":{\"include\":[],\"exclude\":[]}}}'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "server_organizationId_organization_id_fk": { + "name": "server_organizationId_organization_id_fk", + "tableFrom": "server", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "server_sshKeyId_ssh-key_sshKeyId_fk": { + "name": "server_sshKeyId_ssh-key_sshKeyId_fk", + "tableFrom": "server", + "tableTo": "ssh-key", + "columnsFrom": [ + "sshKeyId" + ], + "columnsTo": [ + "sshKeyId" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh-key": { + "name": "ssh-key", + "schema": "", + "columns": { + "sshKeyId": { + "name": "sshKeyId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "privateKey": { + "name": "privateKey", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "publicKey": { + "name": "publicKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lastUsedAt": { + "name": "lastUsedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "ssh-key_organizationId_organization_id_fk": { + "name": "ssh-key_organizationId_organization_id_fk", + "tableFrom": "ssh-key", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain_verified": { + "name": "domain_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "sso_provider_organization_id_organization_id_fk": { + "name": "sso_provider_organization_id_organization_id_fk", + "tableFrom": "sso_provider", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sso_provider_provider_id_unique": { + "name": "sso_provider_provider_id_unique", + "nullsNotDistinct": false, + "columns": [ + "provider_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_tag": { + "name": "project_tag", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tagId": { + "name": "tagId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "project_tag_projectId_project_projectId_fk": { + "name": "project_tag_projectId_project_projectId_fk", + "tableFrom": "project_tag", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_tag_tagId_tag_tagId_fk": { + "name": "project_tag_tagId_tag_tagId_fk", + "tableFrom": "project_tag", + "tableTo": "tag", + "columnsFrom": [ + "tagId" + ], + "columnsTo": [ + "tagId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_project_tag": { + "name": "unique_project_tag", + "nullsNotDistinct": false, + "columns": [ + "projectId", + "tagId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tag": { + "name": "tag", + "schema": "", + "columns": { + "tagId": { + "name": "tagId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "tag_organizationId_organization_id_fk": { + "name": "tag_organizationId_organization_id_fk", + "tableFrom": "tag", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_org_tag_name": { + "name": "unique_org_tag_name", + "nullsNotDistinct": false, + "columns": [ + "organizationId", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "firstName": { + "name": "firstName", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "lastName": { + "name": "lastName", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "isRegistered": { + "name": "isRegistered", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "expirationDate": { + "name": "expirationDate", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "two_factor_enabled": { + "name": "two_factor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "enablePaidFeatures": { + "name": "enablePaidFeatures", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "allowImpersonation": { + "name": "allowImpersonation", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enableEnterpriseFeatures": { + "name": "enableEnterpriseFeatures", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "licenseKey": { + "name": "licenseKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isValidEnterpriseLicense": { + "name": "isValidEnterpriseLicense", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "stripeCustomerId": { + "name": "stripeCustomerId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripeSubscriptionId": { + "name": "stripeSubscriptionId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serversQuantity": { + "name": "serversQuantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sendInvoiceNotifications": { + "name": "sendInvoiceNotifications", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "isEnterpriseCloud": { + "name": "isEnterpriseCloud", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trustedOrigins": { + "name": "trustedOrigins", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "bookmarkedTemplates": { + "name": "bookmarkedTemplates", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "ARRAY[]::text[]" + }, + "onboardingCompletedAt": { + "name": "onboardingCompletedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_provider": { + "name": "vault_provider", + "schema": "", + "columns": { + "vaultProviderId": { + "name": "vaultProviderId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerType": { + "name": "providerType", + "type": "VaultProviderType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "assignments": { + "name": "assignments", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "vault_provider_org_name_idx": { + "name": "vault_provider_org_name_idx", + "columns": [ + { + "expression": "organizationId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vault_provider_organizationId_organization_id_fk": { + "name": "vault_provider_organizationId_organization_id_fk", + "tableFrom": "vault_provider", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.volume_backup": { + "name": "volume_backup", + "schema": "", + "columns": { + "volumeBackupId": { + "name": "volumeBackupId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "volumeName": { + "name": "volumeName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serviceType": { + "name": "serviceType", + "type": "serviceType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'application'" + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serviceName": { + "name": "serviceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "turnOff": { + "name": "turnOff", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cronExpression": { + "name": "cronExpression", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "keepLatestCount": { + "name": "keepLatestCount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redisId": { + "name": "redisId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "libsqlId": { + "name": "libsqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "destinationId": { + "name": "destinationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "volume_backup_applicationId_application_applicationId_fk": { + "name": "volume_backup_applicationId_application_applicationId_fk", + "tableFrom": "volume_backup", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_postgresId_postgres_postgresId_fk": { + "name": "volume_backup_postgresId_postgres_postgresId_fk", + "tableFrom": "volume_backup", + "tableTo": "postgres", + "columnsFrom": [ + "postgresId" + ], + "columnsTo": [ + "postgresId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_mariadbId_mariadb_mariadbId_fk": { + "name": "volume_backup_mariadbId_mariadb_mariadbId_fk", + "tableFrom": "volume_backup", + "tableTo": "mariadb", + "columnsFrom": [ + "mariadbId" + ], + "columnsTo": [ + "mariadbId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_mongoId_mongo_mongoId_fk": { + "name": "volume_backup_mongoId_mongo_mongoId_fk", + "tableFrom": "volume_backup", + "tableTo": "mongo", + "columnsFrom": [ + "mongoId" + ], + "columnsTo": [ + "mongoId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_mysqlId_mysql_mysqlId_fk": { + "name": "volume_backup_mysqlId_mysql_mysqlId_fk", + "tableFrom": "volume_backup", + "tableTo": "mysql", + "columnsFrom": [ + "mysqlId" + ], + "columnsTo": [ + "mysqlId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_redisId_redis_redisId_fk": { + "name": "volume_backup_redisId_redis_redisId_fk", + "tableFrom": "volume_backup", + "tableTo": "redis", + "columnsFrom": [ + "redisId" + ], + "columnsTo": [ + "redisId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_libsqlId_libsql_libsqlId_fk": { + "name": "volume_backup_libsqlId_libsql_libsqlId_fk", + "tableFrom": "volume_backup", + "tableTo": "libsql", + "columnsFrom": [ + "libsqlId" + ], + "columnsTo": [ + "libsqlId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_composeId_compose_composeId_fk": { + "name": "volume_backup_composeId_compose_composeId_fk", + "tableFrom": "volume_backup", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_destinationId_destination_destinationId_fk": { + "name": "volume_backup_destinationId_destination_destinationId_fk", + "tableFrom": "volume_backup", + "tableTo": "destination", + "columnsFrom": [ + "destinationId" + ], + "columnsTo": [ + "destinationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webServerSettings": { + "name": "webServerSettings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "serverIp": { + "name": "serverIp", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "https": { + "name": "https", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "letsEncryptEmail": { + "name": "letsEncryptEmail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sshPrivateKey": { + "name": "sshPrivateKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enableDockerCleanup": { + "name": "enableDockerCleanup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "logCleanupCron": { + "name": "logCleanupCron", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'0 0 * * *'" + }, + "enableLogManagement": { + "name": "enableLogManagement", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "logManagementOrganizationId": { + "name": "logManagementOrganizationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metricsConfig": { + "name": "metricsConfig", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"server\":{\"type\":\"Dokploy\",\"refreshRate\":60,\"port\":4500,\"token\":\"\",\"retentionDays\":2,\"cronJob\":\"\",\"urlCallback\":\"\",\"thresholds\":{\"cpu\":0,\"memory\":0}},\"containers\":{\"refreshRate\":60,\"services\":{\"include\":[],\"exclude\":[]}}}'::jsonb" + }, + "whitelabelingConfig": { + "name": "whitelabelingConfig", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{\"appName\":null,\"appDescription\":null,\"logoUrl\":null,\"faviconUrl\":null,\"customCss\":null,\"loginLogoUrl\":null,\"supportUrl\":null,\"docsUrl\":null,\"errorPageTitle\":null,\"errorPageDescription\":null,\"footerText\":null,\"ogImageUrl\":null}'::jsonb" + }, + "remoteServersOnly": { + "name": "remoteServersOnly", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "buildsConcurrency": { + "name": "buildsConcurrency", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "enforceSSO": { + "name": "enforceSSO", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cleanupCacheApplications": { + "name": "cleanupCacheApplications", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cleanupCacheOnPreviews": { + "name": "cleanupCacheOnPreviews", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cleanupCacheOnCompose": { + "name": "cleanupCacheOnCompose", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "webServerSettings_logManagementOrganizationId_organization_id_fk": { + "name": "webServerSettings_logManagementOrganizationId_organization_id_fk", + "tableFrom": "webServerSettings", + "tableTo": "organization", + "columnsFrom": [ + "logManagementOrganizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.buildType": { + "name": "buildType", + "schema": "public", + "values": [ + "dockerfile", + "heroku_buildpacks", + "paketo_buildpacks", + "nixpacks", + "static", + "railpack" + ] + }, + "public.sourceType": { + "name": "sourceType", + "schema": "public", + "values": [ + "docker", + "git", + "github", + "gitlab", + "bitbucket", + "gitea", + "drop" + ] + }, + "public.backupType": { + "name": "backupType", + "schema": "public", + "values": [ + "database", + "compose" + ] + }, + "public.databaseType": { + "name": "databaseType", + "schema": "public", + "values": [ + "postgres", + "mariadb", + "mysql", + "mongo", + "web-server", + "libsql" + ] + }, + "public.composeType": { + "name": "composeType", + "schema": "public", + "values": [ + "docker-compose", + "stack" + ] + }, + "public.sourceTypeCompose": { + "name": "sourceTypeCompose", + "schema": "public", + "values": [ + "git", + "github", + "gitlab", + "bitbucket", + "gitea", + "raw" + ] + }, + "public.deploymentStatus": { + "name": "deploymentStatus", + "schema": "public", + "values": [ + "running", + "done", + "error", + "cancelled" + ] + }, + "public.DnsProviderType": { + "name": "DnsProviderType", + "schema": "public", + "values": [ + "cloudflare", + "route53", + "porkbun", + "infomaniak", + "ovh" + ] + }, + "public.domainType": { + "name": "domainType", + "schema": "public", + "values": [ + "compose", + "application", + "preview" + ] + }, + "public.gitProviderType": { + "name": "gitProviderType", + "schema": "public", + "values": [ + "github", + "gitlab", + "bitbucket", + "gitea" + ] + }, + "public.LogProviderType": { + "name": "LogProviderType", + "schema": "public", + "values": [ + "loki", + "datadog", + "betterstack", + "elasticsearch", + "splunk_hec", + "aws_cloudwatch" + ] + }, + "public.mountType": { + "name": "mountType", + "schema": "public", + "values": [ + "bind", + "volume", + "file" + ] + }, + "public.serviceType": { + "name": "serviceType", + "schema": "public", + "values": [ + "application", + "postgres", + "mysql", + "mariadb", + "mongo", + "redis", + "compose", + "libsql" + ] + }, + "public.networkDriver": { + "name": "networkDriver", + "schema": "public", + "values": [ + "bridge", + "overlay" + ] + }, + "public.notificationType": { + "name": "notificationType", + "schema": "public", + "values": [ + "slack", + "telegram", + "discord", + "email", + "resend", + "gotify", + "ntfy", + "mattermost", + "pushover", + "custom", + "lark", + "teams" + ] + }, + "public.patchType": { + "name": "patchType", + "schema": "public", + "values": [ + "create", + "update", + "delete" + ] + }, + "public.protocolType": { + "name": "protocolType", + "schema": "public", + "values": [ + "tcp", + "udp" + ] + }, + "public.publishModeType": { + "name": "publishModeType", + "schema": "public", + "values": [ + "ingress", + "host" + ] + }, + "public.RegistryType": { + "name": "RegistryType", + "schema": "public", + "values": [ + "selfHosted", + "cloud" + ] + }, + "public.scheduleType": { + "name": "scheduleType", + "schema": "public", + "values": [ + "application", + "compose", + "server", + "dokploy-server" + ] + }, + "public.shellType": { + "name": "shellType", + "schema": "public", + "values": [ + "bash", + "sh" + ] + }, + "public.serverStatus": { + "name": "serverStatus", + "schema": "public", + "values": [ + "active", + "inactive" + ] + }, + "public.serverType": { + "name": "serverType", + "schema": "public", + "values": [ + "deploy", + "build" + ] + }, + "public.applicationStatus": { + "name": "applicationStatus", + "schema": "public", + "values": [ + "idle", + "running", + "done", + "error" + ] + }, + "public.certificateType": { + "name": "certificateType", + "schema": "public", + "values": [ + "letsencrypt", + "none", + "custom" + ] + }, + "public.sqldNode": { + "name": "sqldNode", + "schema": "public", + "values": [ + "primary", + "replica" + ] + }, + "public.triggerType": { + "name": "triggerType", + "schema": "public", + "values": [ + "push", + "tag" + ] + }, + "public.VaultProviderType": { + "name": "VaultProviderType", + "schema": "public", + "values": [ + "hashicorp", + "infisical", + "aws", + "aws-parameter-store", + "doppler", + "azure", + "scaleway", + "phase" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/dokploy/drizzle/meta/_journal.json b/apps/dokploy/drizzle/meta/_journal.json index c97c2d519..5be52175e 100644 --- a/apps/dokploy/drizzle/meta/_journal.json +++ b/apps/dokploy/drizzle/meta/_journal.json @@ -1380,6 +1380,13 @@ "when": 1788995453103, "tag": "0196_worthless_ravenous", "breakpoints": true + }, + { + "idx": 197, + "version": "7", + "when": 1789042957674, + "tag": "0197_past_omega_red", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index 2ae67ad0d..0e8956b39 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -48,6 +48,7 @@ "@ai-sdk/mistral": "^3.0.20", "@ai-sdk/openai": "^3.0.29", "@ai-sdk/openai-compatible": "^2.0.30", + "@aws-sdk/client-cloudwatch-logs": "^3.1108.0", "@aws-sdk/client-route-53": "^3.1108.0", "@aws-sdk/client-secrets-manager": "^3.1108.0", "@aws-sdk/client-ssm": "3.1108.0", diff --git a/apps/dokploy/pages/dashboard/settings/log-providers.tsx b/apps/dokploy/pages/dashboard/settings/log-providers.tsx new file mode 100644 index 000000000..b9b5d0c16 --- /dev/null +++ b/apps/dokploy/pages/dashboard/settings/log-providers.tsx @@ -0,0 +1,55 @@ +import { validateRequest } from "@dokploy/server"; +import { createServerSideHelpers } from "@trpc/react-query/server"; +import type { GetServerSidePropsContext } from "next"; +import type { ReactElement } from "react"; +import superjson from "superjson"; +import { ShowLogProviders } from "@/components/dashboard/settings/log-management/show-log-providers"; +import { DashboardLayout } from "@/components/layouts/dashboard-layout"; +import { appRouter } from "@/server/api/root"; + +const Page = () => { + return ( +
+ +
+ ); +}; + +export default Page; + +Page.getLayout = (page: ReactElement) => { + return {page}; +}; +export async function getServerSideProps( + ctx: GetServerSidePropsContext<{ serviceId: string }>, +) { + const { req, res } = ctx; + const { user, session } = await validateRequest(req); + if (!user || user.role === "member") { + return { + redirect: { + permanent: false, + destination: "/", + }, + }; + } + const helpers = createServerSideHelpers({ + router: appRouter, + ctx: { + req: req as any, + res: res as any, + db: null as any, + session: session as any, + user: user as any, + }, + transformer: superjson, + }); + await helpers.user.get.prefetch(); + await helpers.settings.isCloud.prefetch(); + + return { + props: { + trpcState: helpers.dehydrate(), + }, + }; +} diff --git a/apps/dokploy/server/api/root.ts b/apps/dokploy/server/api/root.ts index 64d61d3fb..fa3d7d13e 100644 --- a/apps/dokploy/server/api/root.ts +++ b/apps/dokploy/server/api/root.ts @@ -21,6 +21,7 @@ import { giteaRouter } from "./routers/gitea"; import { githubRouter } from "./routers/github"; import { gitlabRouter } from "./routers/gitlab"; import { libsqlRouter } from "./routers/libsql"; +import { logProviderRouter } from "./routers/log-provider"; import { mariadbRouter } from "./routers/mariadb"; import { mongoRouter } from "./routers/mongo"; import { mountRouter } from "./routers/mount"; @@ -85,6 +86,7 @@ export const appRouter = createTRPCRouter({ github: githubRouter, gitlab: gitlabRouter, libsql: libsqlRouter, + logProvider: logProviderRouter, mariadb: mariadbRouter, mongo: mongoRouter, mounts: mountRouter, diff --git a/apps/dokploy/server/api/routers/log-provider.ts b/apps/dokploy/server/api/routers/log-provider.ts new file mode 100644 index 000000000..f49c3eb07 --- /dev/null +++ b/apps/dokploy/server/api/routers/log-provider.ts @@ -0,0 +1,141 @@ +import { + createLogProvider, + findLogProviderById, + findLogProvidersByOrganization, + logProviderAdapters, + removeLogProvider, + sanitizeLogProvider, + testLogProviderConnection, + updateLogProvider, +} from "@dokploy/server"; +import { TRPCError } from "@trpc/server"; +import { audit } from "@/server/api/utils/audit"; +import { + apiCreateLogProvider, + apiFindOneLogProvider, + apiRemoveLogProvider, + apiTestLogProvider, + apiUpdateLogProvider, +} from "@/server/db/schema"; +import { safeSyncVectorAgentsForOrganization } from "@/server/utils/vector-resync"; +import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc"; + +export const logProviderRouter = createTRPCRouter({ + create: withPermission("logProvider", "create") + .input(apiCreateLogProvider) + .mutation(async ({ ctx, input }) => { + const provider = await createLogProvider( + input, + ctx.session.activeOrganizationId, + ); + await audit(ctx, { + action: "create", + resourceType: "logProvider", + resourceId: provider.logProviderId, + resourceName: provider.name, + }); + const syncErrors = await safeSyncVectorAgentsForOrganization( + ctx.session.activeOrganizationId, + ); + return { ...sanitizeLogProvider(provider), syncErrors }; + }), + update: withPermission("logProvider", "create") + .input(apiUpdateLogProvider) + .mutation(async ({ ctx, input }) => { + const { logProviderId, ...rest } = input; + const provider = await findLogProviderById(logProviderId); + if (provider.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not allowed to update this log provider", + }); + } + const updated = await updateLogProvider(logProviderId, rest); + await audit(ctx, { + action: "update", + resourceType: "logProvider", + resourceId: logProviderId, + resourceName: provider.name, + }); + const syncErrors = await safeSyncVectorAgentsForOrganization( + ctx.session.activeOrganizationId, + ); + return { ...sanitizeLogProvider(updated), syncErrors }; + }), + remove: withPermission("logProvider", "delete") + .input(apiRemoveLogProvider) + .mutation(async ({ ctx, input }) => { + const provider = await findLogProviderById(input.logProviderId); + if (provider.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not allowed to delete this log provider", + }); + } + const removed = await removeLogProvider(input.logProviderId); + await audit(ctx, { + action: "delete", + resourceType: "logProvider", + resourceId: provider.logProviderId, + resourceName: provider.name, + }); + const syncErrors = await safeSyncVectorAgentsForOrganization( + ctx.session.activeOrganizationId, + ); + return { ...sanitizeLogProvider(removed), syncErrors }; + }), + all: withPermission("logProvider", "read").query(async ({ ctx }) => { + return await findLogProvidersByOrganization( + ctx.session.activeOrganizationId, + ); + }), + one: withPermission("logProvider", "read") + .input(apiFindOneLogProvider) + .query(async ({ ctx, input }) => { + const provider = await findLogProviderById(input.logProviderId); + if (provider.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not allowed to access this log provider", + }); + } + return provider; + }), + testConnection: withPermission("logProvider", "create") + .input(apiTestLogProvider) + .mutation(async ({ input }) => { + return await testLogProviderConnection({ + providerType: input.providerType, + config: { + logProviderId: "test", + name: input.name ?? "test", + endpoint: input.endpoint ?? null, + apiKey: input.apiKey ?? null, + apiSecret: input.apiSecret ?? null, + extraConfig: input.extraConfig ?? null, + }, + }); + }), + testConnectionById: withPermission("logProvider", "create") + .input(apiFindOneLogProvider) + .mutation(async ({ ctx, input }) => { + const provider = await findLogProviderById(input.logProviderId); + if (provider.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not allowed to access this log provider", + }); + } + return await testLogProviderConnection({ + logProviderId: input.logProviderId, + }); + }), + availableTypes: protectedProcedure.query(() => { + return Object.values(logProviderAdapters).map((adapter) => ({ + type: adapter.type, + label: adapter.label, + docsUrl: adapter.docsUrl, + credentialFields: adapter.credentialFields, + })); + }), +}); diff --git a/apps/dokploy/server/api/routers/organization.ts b/apps/dokploy/server/api/routers/organization.ts index 80c4d4bc6..a334c4afe 100644 --- a/apps/dokploy/server/api/routers/organization.ts +++ b/apps/dokploy/server/api/routers/organization.ts @@ -20,6 +20,7 @@ import { organizationRole, user, } from "@/server/db/schema"; +import { teardownVectorForOrganizationDeletion } from "@/server/utils/vector-resync"; import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc"; export const organizationRouter = createTRPCRouter({ create: protectedProcedure @@ -285,6 +286,8 @@ export const organizationRouter = createTRPCRouter({ }); } + await teardownVectorForOrganizationDeletion(input.organizationId); + const result = await db .delete(organization) .where(eq(organization.id, input.organizationId)); diff --git a/apps/dokploy/server/api/routers/server.ts b/apps/dokploy/server/api/routers/server.ts index a872679c3..fd2321d1a 100644 --- a/apps/dokploy/server/api/routers/server.ts +++ b/apps/dokploy/server/api/routers/server.ts @@ -12,6 +12,7 @@ import { IS_CLOUD, redactServerSshKey, removeDeploymentsByServerId, + removeVectorAgent, serverAudit, serverSetup, serverValidate, @@ -50,6 +51,10 @@ import { server, } from "@/server/db/schema"; import { applyDockerCleanupSchedule } from "@/server/utils/docker-cleanup"; +import { + applyVectorResyncSchedule, + syncVectorAgentAndSchedule, +} from "@/server/utils/vector-resync"; export const serverRouter = createTRPCRouter({ create: withPermission("server", "create") @@ -453,6 +458,73 @@ export const serverRouter = createTRPCRouter({ throw error; } }), + setupLogManagement: withPermission("server", "create") + .input(apiFindOneServer) + .mutation(async ({ input, ctx }) => { + const server = await findServerById(input.serverId); + if (server.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to setup this server", + }); + } + const result = await syncVectorAgentAndSchedule(input.serverId); + await audit(ctx, { + action: "update", + resourceType: "server", + resourceId: input.serverId, + resourceName: server.name, + }); + return result; + }), + updateLogManagement: withPermission("server", "create") + .input( + z.object({ + serverId: z.string().min(1), + enableLogManagement: z.boolean(), + }), + ) + .mutation(async ({ input, ctx }) => { + const server = await findServerById(input.serverId); + if (server.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to update this server", + }); + } + await updateServerById(input.serverId, { + enableLogManagement: input.enableLogManagement, + }); + try { + const result = await syncVectorAgentAndSchedule(input.serverId); + await audit(ctx, { + action: "update", + resourceType: "server", + resourceId: input.serverId, + resourceName: server.name, + }); + return { ...result, enableLogManagement: input.enableLogManagement }; + } catch (error) { + await updateServerById(input.serverId, { + enableLogManagement: server.enableLogManagement, + }); + await syncVectorAgentAndSchedule(input.serverId).catch( + (teardownError) => { + console.error( + `[Vector] Failed to tear down agent for server ${input.serverId} after a failed sync — it may still be running:`, + teardownError, + ); + }, + ); + throw new TRPCError({ + code: "BAD_REQUEST", + message: + error instanceof Error + ? `Failed to sync Vector agent: ${error.message}` + : "Failed to sync Vector agent", + }); + } + }), remove: withPermission("server", "delete") .input(apiRemoveServer) .mutation(async ({ input, ctx }) => { @@ -473,11 +545,28 @@ export const serverRouter = createTRPCRouter({ message: "Server has active services, please delete them first", }); } + let vectorAgentRemovalWarning: string | undefined; + if (currentServer.enableLogManagement) { + await removeVectorAgent({ serverId: input.serverId }).catch( + (error) => { + vectorAgentRemovalWarning = + error instanceof Error ? error.message : String(error); + console.error( + `[Vector] Failed to remove agent for server ${input.serverId} before deletion:`, + error, + ); + }, + ); + await applyVectorResyncSchedule(input.serverId, false); + } await audit(ctx, { action: "delete", resourceType: "server", resourceId: currentServer.serverId, resourceName: currentServer.name, + ...(vectorAgentRemovalWarning + ? { metadata: { vectorAgentRemovalWarning } } + : {}), }); await removeDeploymentsByServerId(currentServer); await deleteServer(input.serverId); @@ -488,7 +577,10 @@ export const serverRouter = createTRPCRouter({ await updateServersBasedOnQuantity(admin.id, admin.serversQuantity); } - return redactServerSshKey(currentServer); + return { + ...redactServerSshKey(currentServer), + vectorAgentRemovalWarning, + }; } catch (error) { throw error; } @@ -511,7 +603,34 @@ export const serverRouter = createTRPCRouter({ message: "Server is inactive", }); } - const currentServer = await updateServerById(input.serverId, { + + const connectionChanged = + server.ipAddress !== input.ipAddress || + server.port !== input.port || + server.username !== input.username || + server.sshKeyId !== input.sshKeyId; + + let vectorAgentWarning: string | undefined; + const recordVectorWarning = (prefix: string, error: unknown) => { + const message = `${prefix}: ${error instanceof Error ? error.message : String(error)}`; + vectorAgentWarning = vectorAgentWarning + ? `${vectorAgentWarning} ${message}` + : message; + console.error(`[Vector] ${message}`); + }; + + if (connectionChanged && server.enableLogManagement) { + await removeVectorAgent({ serverId: input.serverId }).catch( + (error) => { + recordVectorWarning( + "Failed to remove the agent from the old host", + error, + ); + }, + ); + } + + let currentServer = await updateServerById(input.serverId, { ...input, }); @@ -521,13 +640,37 @@ export const serverRouter = createTRPCRouter({ input.enableDockerCleanup, ); + if (connectionChanged && server.enableLogManagement) { + try { + await syncVectorAgentAndSchedule(input.serverId); + } catch (error) { + currentServer = + (await updateServerById(input.serverId, { + enableLogManagement: false, + })) ?? currentServer; + await syncVectorAgentAndSchedule(input.serverId).catch( + (teardownError) => { + console.error( + `[Vector] Failed to tear down agent for server ${input.serverId} after a failed sync — it may still be running:`, + teardownError, + ); + }, + ); + recordVectorWarning( + "Failed to install the agent on the new host, log management has been turned off for this server", + error, + ); + } + } + await audit(ctx, { action: "update", resourceType: "server", resourceId: input.serverId, resourceName: server.name, + ...(vectorAgentWarning ? { metadata: { vectorAgentWarning } } : {}), }); - return currentServer; + return { ...currentServer, vectorAgentWarning }; } catch (error) { throw error; } diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts index db2951ae4..5caeae96e 100644 --- a/apps/dokploy/server/api/routers/settings.ts +++ b/apps/dokploy/server/api/routers/settings.ts @@ -4,6 +4,7 @@ import { checkPortInUse, checkPostgresHealth, checkTraefikHealth, + claimWebServerLogManagement, cleanupAll, cleanupAllBackground, cleanupBuilders, @@ -71,6 +72,7 @@ import { } from "@/server/db/schema"; import { cleanAllDeploymentQueue } from "@/server/queues/queueSetup"; import { removeJob, schedule } from "@/server/utils/backup"; +import { syncWebVectorAgentAndSchedule } from "@/server/utils/vector-resync"; import packageInfo from "../../../package.json"; import { appRouter } from "../root"; import { @@ -410,6 +412,75 @@ export const settingsRouter = createTRPCRouter({ return true; }), + updateLogManagement: adminProcedure + .input(z.object({ enableLogManagement: z.boolean() })) + .mutation(async ({ input, ctx }) => { + if (IS_CLOUD) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "This feature is only available for self-hosted instances", + }); + } + + const activeOrganizationId = ctx.session.activeOrganizationId; + const current = await getWebServerSettings(); + const currentOwner = current?.logManagementOrganizationId ?? null; + + if (currentOwner && currentOwner !== activeOrganizationId) { + throw new TRPCError({ + code: "FORBIDDEN", + message: input.enableLogManagement + ? "The local Vector agent is already used by another organization on this instance — disable it there first." + : "You are not authorized to disable the local Vector agent — it belongs to another organization.", + }); + } + + const previous = { + enableLogManagement: !!current?.enableLogManagement, + logManagementOrganizationId: currentOwner, + }; + + const claimed = await claimWebServerLogManagement( + activeOrganizationId, + input.enableLogManagement, + ); + if (!claimed) { + throw new TRPCError({ + code: "FORBIDDEN", + message: + "The local Vector agent was just claimed by another organization — try again.", + }); + } + + try { + const result = + await syncWebVectorAgentAndSchedule(activeOrganizationId); + await audit(ctx, { + action: "update", + resourceType: "settings", + resourceName: "log-management", + }); + return { ...result, enableLogManagement: input.enableLogManagement }; + } catch (error) { + await updateWebServerSettings(previous); + await syncWebVectorAgentAndSchedule(activeOrganizationId).catch( + (teardownError) => { + console.error( + "[Vector] Failed to reconcile local agent after a failed sync:", + teardownError, + ); + }, + ); + throw new TRPCError({ + code: "BAD_REQUEST", + message: + error instanceof Error + ? `Failed to sync local Vector agent: ${error.message}` + : "Failed to sync local Vector agent", + }); + } + }), + updateRemoteServersOnly: enterpriseProcedure .input(z.object({ remoteServersOnly: z.boolean() })) .mutation(async ({ input, ctx }) => { diff --git a/apps/dokploy/server/server.ts b/apps/dokploy/server/server.ts index 5fe048c92..f57c2264c 100644 --- a/apps/dokploy/server/server.ts +++ b/apps/dokploy/server/server.ts @@ -16,6 +16,7 @@ import { import { config } from "dotenv"; import next from "next"; import packageInfo from "../package.json"; +import { initVectorResyncSchedules } from "./utils/vector-resync"; import { setupDockerContainerLogsWebSocketServer } from "./wss/docker-container-logs"; import { setupDockerContainerTerminalWebSocketServer } from "./wss/docker-container-terminal"; import { setupDockerStatsMonitoringSocketServer } from "./wss/docker-stats"; @@ -65,7 +66,10 @@ void app.prepare().then(async () => { await initSchedules(); await initCancelDeployments(); await initVolumeBackupsCronJobs(); + await initVectorResyncSchedules(); await sendDokployRestartNotifications(); + } else if (IS_CLOUD) { + await initVectorResyncSchedules(); } await initEnterpriseBackupCronJobs(); diff --git a/apps/dokploy/server/utils/vector-resync.ts b/apps/dokploy/server/utils/vector-resync.ts new file mode 100644 index 000000000..4668e1699 --- /dev/null +++ b/apps/dokploy/server/utils/vector-resync.ts @@ -0,0 +1,249 @@ +import { + findAllServersWithLogManagementEnabled, + findServersWithLogManagementEnabled, + getWebServerSettings, + hasEnabledLogProvider, + IS_CLOUD, + loadVectorOrgData, + removeVectorAgent, + removeWebVectorAgent, + syncVectorAgent, + syncVectorConfig, + syncWebVectorAgent, + syncWebVectorConfig, + updateWebServerSettings, + VECTOR_RESYNC_CRON_JOB, + type VectorOrgData, +} from "@dokploy/server"; +import { TRPCError } from "@trpc/server"; +import { scheduledJobs, scheduleJob } from "node-schedule"; + +export const applyVectorResyncSchedule = async ( + serverId: string, + enable: boolean, +) => { + const jobName = `vector-resync:${serverId}`; + if (IS_CLOUD) { + console.warn( + `[Vector] Periodic resync for server ${serverId} is running in-process, not via the Cloud Jobs service (no handler for "vector-resync" yet).`, + ); + } + if (enable) { + scheduleJob(jobName, VECTOR_RESYNC_CRON_JOB, async () => { + try { + await syncVectorConfig({ serverId }); + } catch (error) { + console.error(`[Vector] Resync failed for server ${serverId}:`, error); + if (error instanceof TRPCError && error.code === "NOT_FOUND") { + scheduledJobs[jobName]?.cancel(); + } + } + }); + } else { + scheduledJobs[jobName]?.cancel(); + } +}; + +const WEB_VECTOR_RESYNC_JOB_NAME = "vector-resync-web"; + +export const applyWebVectorResyncSchedule = async ( + organizationId: string, + enable: boolean, +) => { + if (enable) { + scheduleJob( + WEB_VECTOR_RESYNC_JOB_NAME, + VECTOR_RESYNC_CRON_JOB, + async () => { + try { + await syncWebVectorConfig(organizationId); + } catch (error) { + console.error("[Vector] Local resync failed:", error); + } + }, + ); + } else { + scheduledJobs[WEB_VECTOR_RESYNC_JOB_NAME]?.cancel(); + } +}; + +export const initVectorResyncSchedules = async () => { + let servers: Awaited< + ReturnType + >; + try { + servers = await findAllServersWithLogManagementEnabled(); + } catch (error) { + console.error( + "[Vector] Failed to load servers for resync schedules:", + error, + ); + return; + } + const hasProviderByOrg = new Map< + string, + ReturnType + >(); + const hasProviderCached = (organizationId: string) => { + let result = hasProviderByOrg.get(organizationId); + if (!result) { + result = hasEnabledLogProvider(organizationId); + hasProviderByOrg.set(organizationId, result); + } + return result; + }; + + await Promise.all( + servers.map(async (s) => { + try { + if (await hasProviderCached(s.organizationId)) { + await applyVectorResyncSchedule(s.serverId, true); + } + } catch (error) { + console.error( + `[Vector] Failed to re-register resync schedule for server ${s.serverId}:`, + error, + ); + } + }), + ); + + if (!IS_CLOUD) { + try { + const settings = await getWebServerSettings(); + const organizationId = settings?.logManagementOrganizationId; + if ( + settings?.enableLogManagement && + organizationId && + (await hasEnabledLogProvider(organizationId)) + ) { + await applyWebVectorResyncSchedule(organizationId, true); + } + } catch (error) { + console.error( + "[Vector] Failed to re-register local resync schedule:", + error, + ); + } + } +}; + +export const syncVectorAgentAndSchedule = async ( + serverId: string, + preloaded?: VectorOrgData, +) => { + const { installed } = await syncVectorAgent({ serverId, preloaded }); + await applyVectorResyncSchedule(serverId, installed); + return { installed }; +}; + +export const syncWebVectorAgentAndSchedule = async ( + organizationId: string, + preloaded?: VectorOrgData, +) => { + const { installed } = await syncWebVectorAgent(preloaded); + await applyWebVectorResyncSchedule(organizationId, installed); + return { installed }; +}; + +export const syncVectorAgentsForOrganization = async ( + organizationId: string, +): Promise> => { + const servers = await findServersWithLogManagementEnabled(organizationId); + const webOwnsThisOrg = + !IS_CLOUD && + (await getWebServerSettings())?.logManagementOrganizationId === + organizationId; + + if (servers.length === 0 && !webOwnsThisOrg) { + return []; + } + + const preloaded = await loadVectorOrgData(organizationId); + + const targets: Array<{ + id: string; + run: () => Promise<{ installed: boolean }>; + }> = [ + ...servers.map((s) => ({ + id: s.serverId, + run: () => syncVectorAgentAndSchedule(s.serverId, preloaded), + })), + ...(webOwnsThisOrg + ? [ + { + id: "web", + run: () => syncWebVectorAgentAndSchedule(organizationId, preloaded), + }, + ] + : []), + ]; + + const results = await Promise.allSettled(targets.map((t) => t.run())); + + const errors: Array<{ serverId: string; error: string }> = []; + results.forEach((result, index) => { + if (result.status === "rejected") { + const target = targets[index]; + if (target) { + errors.push({ + serverId: target.id, + error: + result.reason instanceof Error + ? result.reason.message + : String(result.reason), + }); + } + } + }); + return errors; +}; + +export const safeSyncVectorAgentsForOrganization = async ( + organizationId: string, +) => { + try { + return await syncVectorAgentsForOrganization(organizationId); + } catch (error) { + return [ + { + serverId: "unknown", + error: error instanceof Error ? error.message : String(error), + }, + ]; + } +}; + +export const teardownVectorForOrganizationDeletion = async ( + organizationId: string, +): Promise => { + const servers = await findServersWithLogManagementEnabled(organizationId); + await Promise.all( + servers.map(async (s) => { + await removeVectorAgent({ serverId: s.serverId }).catch((error) => { + console.error( + `[Vector] Failed to remove agent for server ${s.serverId} before deleting organization ${organizationId}:`, + error, + ); + }); + await applyVectorResyncSchedule(s.serverId, false); + }), + ); + + if (!IS_CLOUD) { + const settings = await getWebServerSettings(); + if (settings?.logManagementOrganizationId === organizationId) { + await removeWebVectorAgent().catch((error) => { + console.error( + `[Vector] Failed to remove local agent before deleting organization ${organizationId}:`, + error, + ); + }); + await applyWebVectorResyncSchedule(organizationId, false); + await updateWebServerSettings({ + enableLogManagement: false, + logManagementOrganizationId: null, + }); + } + } +}; diff --git a/docs/log-management.md b/docs/log-management.md new file mode 100644 index 000000000..d9a60cb82 --- /dev/null +++ b/docs/log-management.md @@ -0,0 +1,138 @@ +# Log Management + +> **Nota para quien mergee esto:** este contenido vive acá porque no había un checkout local del +> repo de docs públicas (`docs.dokploy.com`) disponible al escribir esta feature. Migrar este +> archivo a ese repo (sección "Core" o similar, junto a Registry/Monitoring) y borrarlo de acá. + +## Qué es + +Dokploy puede shippear los logs de los containers de un server a un proveedor externo de log +management — hoy: **Grafana Loki**, **Datadog Logs**, **Better Stack** (ex-Logtail), +**Elasticsearch / OpenSearch**, **Splunk (HTTP Event Collector)** y **AWS CloudWatch Logs**. Antes de +esta feature, la única forma de ver logs era el stream en vivo por WebSocket en el dashboard, sin +historial ni persistencia. Esta feature no reemplaza ese viewer en vivo — sigue funcionando igual +— agrega la posibilidad de mandar una copia de los logs a un sistema externo para +retención/búsqueda/alerting. + +Por debajo corre [Vector](https://vector.dev) (Apache-2.0), un único binario, como un agente por +server registrado. Dokploy no implementa clientes HTTP para cada proveedor — solo traduce +credenciales guardadas en el dashboard a un bloque de configuración de Vector (un "sink"). + +## Cómo activarla + +Hacen falta **dos cosas**, ambas apagadas por default: + +1. **Al menos un log provider configurado**, en Settings → Log Management. Elegís el tipo + (Loki/Datadog/Better Stack/Elasticsearch/OpenSearch/Splunk/AWS CloudWatch), completás las + credenciales que pida ese tipo, y podés probar la + conexión antes de guardar. +2. **El toggle "Log Management" prendido** en cada lugar donde quieras shippear logs: + - Por server registrado: en el diálogo de acciones del server ("Server Actions"), al lado del + toggle de Docker Cleanup. + - **En la máquina donde corre Dokploy** (self-hosted únicamente — no existe en Dokploy Cloud, + donde esa máquina es infraestructura de Dokploy, no del cliente): en Settings → Web Server, + al lado del toggle de Docker Cleanup local. La primera organización que lo prende "reclama" + ese host — otra organización de la misma instancia no puede prenderlo hasta que la primera lo + apague (rechazo explícito, no se pisan silenciosamente). + +Si cualquiera de las dos condiciones no se cumple, el agente Vector **no corre en absoluto** en +ese host — no consume CPU/memoria/disco, y nada más del dashboard se ve afectado (el streaming en +vivo de logs sigue funcionando igual). + +**Host local en una instancia self-hosted multi-organización:** Vector lee el socket de Docker +completo, sin distinguir de qué organización es cada container — pero solo el agente local (el +registrado por `server` siempre pertenece a una sola organización, sin este problema). Para +evitar que le lleguen logs de otra organización a los providers del dueño del host, el pipeline +del agente local descarta todo evento que no matchee una app de la organización dueña (filtro +`dokploy_scope_local_only` en `vector-setup.ts`) antes de que llegue a cualquier sink — esto +también implica que, a diferencia del agente por-server, containers de terceros (no gestionados +por Dokploy) corriendo en la misma máquina ya no shippean via el agente local. + +Un log provider es a nivel de organización: si tenés varios servers con el toggle prendido, todos +ellos shippean a todos los providers habilitados. No hay (todavía) una asociación +provider-por-server — es una mejora futura posible si hace falta más granularidad. + +### Scoping por proyecto/entorno/aplicación + +Aunque Vector ve **todos** los containers del host (no solo los de Dokploy), cada evento que sí +corresponde a una app gestionada por Dokploy llega taggeado con: + +- `dokploy_organization` — siempre presente. +- `dokploy_project` / `dokploy_project_id` +- `dokploy_environment` / `dokploy_environment_id` +- `dokploy_application` / `dokploy_application_id` + +Un container de terceros (no gestionado por Dokploy) corriendo en el mismo host sigue +shippeando igual, simplemente sin esos 6 campos. + +Esta tabla de scoping se recalcula cada vez que se toca algo de log management (activar/crear/ +editar un provider, prender el toggle de un server) y además cada 15 minutos como red de +seguridad, para que una app creada después de la configuración inicial no quede sin estos tags +indefinidamente. + +## Cómo agregar un provider nuevo (guía para contribuidores) + +Toda la lógica de un provider vive en un solo archivo: +`packages/server/src/services/log-management/providers/.ts`, implementando la interfaz +`LogProviderAdapter` (`packages/server/src/services/log-management/types.ts`). **No hace falta +tocar el router tRPC ni la UI** — la UI (`logProvider.availableTypes`) lista los providers +disponibles dinámicamente a partir del registry de adapters, y el orquestador de Vector +(`packages/server/src/setup/vector-setup.ts`) resuelve el resto de forma genérica. + +Hay dos caminos, según si Vector ya tiene un sink nativo para tu backend: + +### Camino 1: tu backend ya tiene un sink en Vector + +Es el caso de Loki, Datadog, Elasticsearch/OpenSearch, Splunk HEC y AWS CloudWatch Logs. Mirá la +[lista de sinks de Vector](https://vector.dev/docs/reference/configuration/sinks/) +— si tu backend está ahí, este es tu camino. + +1. Agregar el tipo al enum de Drizzle: `logProviderType` en + `packages/server/src/db/schema/log-provider.ts`, + correr + `pnpm --filter dokploy migration:generate` y commitear el SQL generado. +2. Ampliar el union `LogProviderType` en `services/log-management/types.ts`. +3. Crear `providers/.ts`: + - `credentialFields`: qué campos pedirle al usuario. `key` tiene que ser exactamente + `"endpoint"` | `"apiKey"` | `"apiSecret"` (las 3 únicas columnas con espacio propio en la + tabla) o, si tu backend necesita algo que no encaja en esas 3, cualquier otro nombre — ese + valor termina en `extraConfig` (jsonb). **Este `key` no es solo para mostrar en la UI: la UI + genérica lo usa para decidir dónde guardar el valor** (columna directa vs. `extraConfig`) — + ver `providers/loki.ts` para el caso simple (`endpoint`) y `providers/betterstack.ts` para + el comentario completo sobre este punto (ahí hubo un bug real por esto durante el desarrollo: + usar un `key` "bonito" que no coincidía con la columna real). + - `toVectorSink(config, sinkId, inputId)`: devolvé el bloque de sink de Vector con tus + credenciales mapeadas. **Siempre incluir `buffer: { type: "disk", max_size, when_full }`** + (ver `DEFAULT_DISK_BUFFER` en `types.ts` para el default) — el default de Vector es buffer + en memoria, que no protege contra un backend caído. + - `testConnection(config)` (opcional pero recomendado): un ping barato para validar + credenciales antes de guardar. +4. Registrar en `providers/registry.ts` (`logProviderAdapters`) — una línea. +5. Tests unitarios junto a los de `loki`/`datadog`/`betterstack` + (`apps/dokploy/__test__/log-management/log-provider-adapters.test.ts`) verificando la forma + del sink generado. + +### Camino 2: tu backend NO tiene sink nativo en Vector + +Es el caso de Better Stack — usar `providers/betterstack.ts` como plantilla. Mismos pasos que +arriba, con dos diferencias: + +- El sink usa el genérico `http` (o `socket`/`file` si tu backend expone algo distinto a HTTP). +- Si tu backend espera el evento en una forma distinta a la que produce el scoping (ej. un + nombre de campo distinto para el timestamp), implementar `toVectorTransform(config, + transformId, scopeTransformId)` — un paso VRL intermedio entre el scoping y tu sink. El + orquestador (`vector-setup.ts`) ya sabe encadenar `scope → tu transform → tu sink` cuando este + método existe, y `scope → sink` directo cuando no — no hay que tocar el orquestador. + +## Decisiones y límites conocidos + +- **Un server = un nodo Swarm de un solo nodo, típicamente.** Dokploy no modela clusters Swarm + multi-nodo (cada `server` registrado es una máquina SSH independiente). Si tu server participa + de un swarm con workers que no están registrados como su propio `server` en Dokploy, el agente + Vector puede terminar programado en un nodo sin la config — fuera de alcance del MVP actual. +- **No hay viewer de logs históricos embebido en el dashboard** — esta feature es solo shipping. + Para ver los logs, usar la UI del provider externo (Grafana, Datadog, Better Stack, Kibana/ + OpenSearch Dashboards, Splunk, CloudWatch Logs Insights). +- **`logProvider` es free tier**, no enterprise-only — a diferencia de `registry`/`server`/ + `domain`/`monitoring`, disponible para cualquier organización sin importar el plan. +- **Deployment en la misma máquina de Dokploy**: soportado self-hosted (Settings → Web Server), + nunca en Dokploy Cloud — ver "Cómo activarla" arriba para el caveat multi-organización. diff --git a/packages/server/package.json b/packages/server/package.json index bd5f0c53d..77864cdd9 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -37,6 +37,7 @@ "@ai-sdk/mistral": "^3.0.20", "@ai-sdk/openai": "^3.0.29", "@ai-sdk/openai-compatible": "^2.0.30", + "@aws-sdk/client-cloudwatch-logs": "^3.1108.0", "@aws-sdk/client-route-53": "^3.1108.0", "@aws-sdk/client-secrets-manager": "^3.1108.0", "@aws-sdk/client-ssm": "3.1108.0", diff --git a/packages/server/src/constants/index.ts b/packages/server/src/constants/index.ts index c303041f3..8f8de52f9 100644 --- a/packages/server/src/constants/index.ts +++ b/packages/server/src/constants/index.ts @@ -13,6 +13,8 @@ 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]; @@ -134,6 +136,7 @@ export const paths = (isServer = false) => { CERTIFICATES_PATH: `${DYNAMIC_TRAEFIK_PATH}/certificates`, MONITORING_PATH: `${BASE_PATH}/monitoring`, REGISTRY_PATH: `${BASE_PATH}/registry`, + VECTOR_PATH: `${BASE_PATH}/vector`, SCHEDULES_PATH: `${BASE_PATH}/schedules`, VOLUME_BACKUPS_PATH: `${BASE_PATH}/volume-backups`, VOLUME_BACKUP_LOCK_PATH: `${BASE_PATH}/volume-backup-lock`, diff --git a/packages/server/src/db/schema/audit-log.ts b/packages/server/src/db/schema/audit-log.ts index a1cec1910..27783099c 100644 --- a/packages/server/src/db/schema/audit-log.ts +++ b/packages/server/src/db/schema/audit-log.ts @@ -94,4 +94,5 @@ export type AuditResourceType = | "compose" | "network" | "vaultProvider" - | "dnsProvider"; + | "dnsProvider" + | "logProvider"; diff --git a/packages/server/src/db/schema/index.ts b/packages/server/src/db/schema/index.ts index f17a12473..87cd4e5ef 100644 --- a/packages/server/src/db/schema/index.ts +++ b/packages/server/src/db/schema/index.ts @@ -17,6 +17,7 @@ export * from "./gitea"; export * from "./github"; export * from "./gitlab"; export * from "./libsql"; +export * from "./log-provider"; export * from "./mariadb"; export * from "./mongo"; export * from "./mount"; diff --git a/packages/server/src/db/schema/log-provider.ts b/packages/server/src/db/schema/log-provider.ts new file mode 100644 index 000000000..6ac15bd77 --- /dev/null +++ b/packages/server/src/db/schema/log-provider.ts @@ -0,0 +1,87 @@ +import { boolean, jsonb, pgEnum, pgTable, text } from "drizzle-orm/pg-core"; +import { createInsertSchema } from "drizzle-zod"; +import { nanoid } from "nanoid"; +import { z } from "zod"; +import { organization } from "./account"; +import { encryptedText } from "./utils"; + +export const logProviderType = pgEnum("LogProviderType", [ + "loki", + "datadog", + "betterstack", + "elasticsearch", + "splunk_hec", + "aws_cloudwatch", +]); + +export const logProvider = pgTable("logProvider", { + logProviderId: text("logProviderId") + .notNull() + .primaryKey() + .$defaultFn(() => nanoid()), + name: text("name").notNull(), + providerType: logProviderType("providerType").notNull(), + endpoint: encryptedText("endpoint"), + apiKey: encryptedText("apiKey"), + apiSecret: encryptedText("apiSecret"), + extraConfig: jsonb("extraConfig").$type>(), + enabled: boolean("enabled").notNull().default(true), + createdAt: text("createdAt") + .notNull() + .$defaultFn(() => new Date().toISOString()), + organizationId: text("organizationId") + .notNull() + .references(() => organization.id, { onDelete: "cascade" }), +}); + +const createSchema = createInsertSchema(logProvider, { + logProviderId: z.string().min(1), + name: z.string().min(1), + providerType: z.enum([ + "loki", + "datadog", + "betterstack", + "elasticsearch", + "splunk_hec", + "aws_cloudwatch", + ]), + endpoint: z + .string() + .min(1) + .refine((value) => !/\s/.test(value), { + message: "Endpoint cannot contain whitespace", + }) + .nullable() + .optional(), + apiKey: z.string().min(1).nullable().optional(), + apiSecret: z.string().min(1).nullable().optional(), + extraConfig: z.record(z.string(), z.unknown()).nullable().optional(), + enabled: z.boolean().optional(), + organizationId: z.string().min(1), +}); + +export const apiCreateLogProvider = createSchema + .pick({ + name: true, + providerType: true, + endpoint: true, + apiKey: true, + apiSecret: true, + extraConfig: true, + enabled: true, + }) + .required({ name: true, providerType: true }); + +export const apiUpdateLogProvider = apiCreateLogProvider.partial().extend({ + logProviderId: z.string().min(1), +}); + +export const apiRemoveLogProvider = z.object({ + logProviderId: z.string().min(1), +}); + +export const apiFindOneLogProvider = z.object({ + logProviderId: z.string().min(1), +}); + +export const apiTestLogProvider = apiCreateLogProvider.partial({ name: true }); diff --git a/packages/server/src/db/schema/server.ts b/packages/server/src/db/schema/server.ts index d888c51f6..aa5209631 100644 --- a/packages/server/src/db/schema/server.ts +++ b/packages/server/src/db/schema/server.ts @@ -42,6 +42,7 @@ export const server = pgTable("server", { .notNull() .$defaultFn(() => generateAppName("server")), enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(false), + enableLogManagement: boolean("enableLogManagement").notNull().default(false), buildsConcurrency: integer("buildsConcurrency").notNull().default(1), createdAt: text("createdAt").notNull(), organizationId: text("organizationId") diff --git a/packages/server/src/db/schema/web-server-settings.ts b/packages/server/src/db/schema/web-server-settings.ts index ab513e283..ac63844c5 100644 --- a/packages/server/src/db/schema/web-server-settings.ts +++ b/packages/server/src/db/schema/web-server-settings.ts @@ -10,6 +10,7 @@ import { import { createInsertSchema } from "drizzle-zod"; import { nanoid } from "nanoid"; import { z } from "zod"; +import { organization } from "./account"; import { certificateType } from "./shared"; export const webServerSettings = pgTable("webServerSettings", { @@ -26,6 +27,11 @@ 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), + logManagementOrganizationId: text("logManagementOrganizationId").references( + () => organization.id, + { onDelete: "set null" }, + ), // Metrics Configuration metricsConfig: jsonb("metricsConfig") .$type<{ diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index cd7bef41f..7b31ade95 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -27,6 +27,9 @@ export * from "./services/gitea"; export * from "./services/github"; export * from "./services/gitlab"; export * from "./services/libsql"; +export * from "./services/log-management/providers/registry"; +export * from "./services/log-management/service"; +export * from "./services/log-management/types"; export * from "./services/mariadb"; export * from "./services/mongo"; export * from "./services/mount"; @@ -68,6 +71,7 @@ export * from "./setup/server-setup"; export * from "./setup/server-validate"; export * from "./setup/setup"; export * from "./setup/traefik-setup"; +export * from "./setup/vector-setup"; export * from "./templates/processors"; export * from "./utils/access-log/handler"; export { diff --git a/packages/server/src/lib/access-control.ts b/packages/server/src/lib/access-control.ts index 05258ec41..e68acf513 100644 --- a/packages/server/src/lib/access-control.ts +++ b/packages/server/src/lib/access-control.ts @@ -28,6 +28,7 @@ export const statements = { gitProviders: ["read", "create", "delete"], traefikFiles: ["read", "write"], api: ["read"], + logProvider: ["read", "create", "delete"], // Enterprise-only resources (custom roles only) volume: ["read", "create", "delete"], @@ -119,6 +120,7 @@ export const ownerRole = ac.newRole({ auditLog: ["read"], vaultProvider: ["read", "create", "update", "delete"], dnsProvider: ["read", "create", "update", "delete"], + logProvider: ["read", "create", "delete"], }); /** @@ -158,6 +160,7 @@ export const adminRole = ac.newRole({ auditLog: ["read"], vaultProvider: ["read", "create", "update", "delete"], dnsProvider: ["read", "create", "update", "delete"], + logProvider: ["read", "create", "delete"], }); /** @@ -198,6 +201,7 @@ export const memberRole = ac.newRole({ certificate: [], destination: [], notification: [], + logProvider: [], tag: ["read"], auditLog: [], // Members need provider/secret names for env editor autocomplete; values are never exposed diff --git a/packages/server/src/services/log-management/providers/aws-cloudwatch.ts b/packages/server/src/services/log-management/providers/aws-cloudwatch.ts new file mode 100644 index 000000000..53de77d9e --- /dev/null +++ b/packages/server/src/services/log-management/providers/aws-cloudwatch.ts @@ -0,0 +1,114 @@ +import { + CloudWatchLogsClient, + DescribeLogGroupsCommand, +} from "@aws-sdk/client-cloudwatch-logs"; +import type { + LogProviderAdapter, + LogProviderRuntimeConfig, + VectorSinkConfig, +} from "../types"; +import { DEFAULT_DISK_BUFFER, LOG_PROVIDER_REQUEST_TIMEOUT_MS } from "../types"; + +const DEFAULT_STREAM_TEMPLATE = "{{ container_name }}"; + +export const awsCloudwatchAdapter: LogProviderAdapter = { + type: "aws_cloudwatch", + label: "AWS CloudWatch Logs", + docsUrl: + "https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/WhatIsCloudWatchLogs.html", + credentialFields: [ + { + key: "apiKey", + label: "Access Key ID", + type: "text", + required: true, + }, + { + key: "apiSecret", + label: "Secret Access Key", + type: "password", + required: true, + }, + { + key: "region", + label: "Region", + type: "text", + required: true, + placeholder: "us-east-1", + }, + { + key: "logGroup", + label: "Log Group Name", + type: "text", + required: true, + placeholder: "/dokploy/logs", + helpText: "Created automatically on first write if it doesn't exist.", + fullWidth: true, + }, + { + key: "logStream", + label: "Log Stream Name Template", + type: "text", + required: false, + placeholder: DEFAULT_STREAM_TEMPLATE, + helpText: + "Vector event template. Defaults to one stream per container name if left blank.", + fullWidth: true, + }, + ], + toVectorSink( + config: LogProviderRuntimeConfig, + _sinkId: string, + inputId: string, + ): VectorSinkConfig { + const logGroup = config.extraConfig?.logGroup; + const logStream = config.extraConfig?.logStream; + const region = config.extraConfig?.region; + return { + type: "aws_cloudwatch_logs", + inputs: [inputId], + group_name: typeof logGroup === "string" ? logGroup : "", + stream_name: + typeof logStream === "string" && logStream.length > 0 + ? logStream + : DEFAULT_STREAM_TEMPLATE, + region: typeof region === "string" ? region : "", + encoding: { codec: "json" }, + auth: { + access_key_id: config.apiKey ?? "", + secret_access_key: config.apiSecret ?? "", + }, + buffer: DEFAULT_DISK_BUFFER, + }; + }, + async testConnection(config: LogProviderRuntimeConfig): Promise { + const region = config.extraConfig?.region; + const logGroup = config.extraConfig?.logGroup; + if ( + !config.apiKey || + !config.apiSecret || + typeof region !== "string" || + !region || + typeof logGroup !== "string" || + !logGroup + ) { + throw new Error( + "AWS access key, secret key, region and log group are required", + ); + } + const client = new CloudWatchLogsClient({ + region, + credentials: { + accessKeyId: config.apiKey, + secretAccessKey: config.apiSecret, + }, + }); + await client.send( + new DescribeLogGroupsCommand({ + logGroupNamePrefix: logGroup, + limit: 1, + }), + { abortSignal: AbortSignal.timeout(LOG_PROVIDER_REQUEST_TIMEOUT_MS) }, + ); + }, +}; diff --git a/packages/server/src/services/log-management/providers/betterstack.ts b/packages/server/src/services/log-management/providers/betterstack.ts new file mode 100644 index 000000000..fe102b5a0 --- /dev/null +++ b/packages/server/src/services/log-management/providers/betterstack.ts @@ -0,0 +1,88 @@ +import type { + LogProviderAdapter, + LogProviderRuntimeConfig, + VectorSinkConfig, + VectorTransformConfig, +} from "../types"; +import { + DEFAULT_DISK_BUFFER, + logProviderFetch, + normalizeEndpointUrl, +} from "../types"; + +export const betterStackAdapter: LogProviderAdapter = { + type: "betterstack", + label: "Better Stack (Logtail)", + docsUrl: "https://betterstack.com/docs/logs/vector/", + credentialFields: [ + { + key: "apiKey", + label: "Source Token", + type: "password", + required: true, + helpText: + "Bearer token from your Better Stack source. Testing the connection sends a real test log to your account.", + fullWidth: true, + }, + { + key: "endpoint", + label: "Ingesting Host", + type: "url", + required: true, + placeholder: "in.logs.betterstack.com", + helpText: + "Shown next to the Source Token when you create the source. Paste it as-is, no https:// needed.", + }, + ], + toVectorTransform( + _config: LogProviderRuntimeConfig, + _transformId: string, + scopeTransformId: string, + ): VectorTransformConfig { + return { + type: "remap", + inputs: [scopeTransformId], + source: ".dt = del(.timestamp)", + }; + }, + toVectorSink( + config: LogProviderRuntimeConfig, + _sinkId: string, + inputId: string, + ): VectorSinkConfig { + return { + type: "http", + inputs: [inputId], + method: "post", + uri: `${normalizeEndpointUrl(config.endpoint ?? "").replace(/\/$/, "")}/`, + encoding: { codec: "json" }, + compression: "gzip", + auth: { strategy: "bearer", token: config.apiKey ?? "" }, + buffer: DEFAULT_DISK_BUFFER, + }; + }, + async testConnection(config: LogProviderRuntimeConfig): Promise { + if (!config.endpoint || !config.apiKey) { + throw new Error( + "Better Stack ingestingHost and sourceToken are required", + ); + } + const uri = `${normalizeEndpointUrl(config.endpoint).replace(/\/$/, "")}/`; + const response = await logProviderFetch(uri, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${config.apiKey}`, + }, + body: JSON.stringify({ + message: "dokploy-log-provider-test", + dokploy_test: true, + }), + }); + if (!response.ok) { + throw new Error( + `Better Stack test event failed with status ${response.status}`, + ); + } + }, +}; diff --git a/packages/server/src/services/log-management/providers/datadog.ts b/packages/server/src/services/log-management/providers/datadog.ts new file mode 100644 index 000000000..0683e202e --- /dev/null +++ b/packages/server/src/services/log-management/providers/datadog.ts @@ -0,0 +1,90 @@ +import type { + LogProviderAdapter, + LogProviderRuntimeConfig, + VectorSinkConfig, + VectorTransformConfig, +} from "../types"; +import { DEFAULT_DISK_BUFFER, logProviderFetch } from "../types"; + +const DDTAGS_VRL = ` +tags = [] +if .dokploy_project != "" { tags = push(tags, "dokploy_project:" + replace(to_string!(.dokploy_project), ",", "_")) } +if .dokploy_environment != "" { tags = push(tags, "dokploy_environment:" + replace(to_string!(.dokploy_environment), ",", "_")) } +if .dokploy_application != "" { tags = push(tags, "dokploy_application:" + replace(to_string!(.dokploy_application), ",", "_")) } +if .dokploy_organization != "" { tags = push(tags, "dokploy_organization:" + replace(to_string!(.dokploy_organization), ",", "_")) } +.ddtags = join!(tags, ",") +`.trim(); + +const resolveSite = (config: LogProviderRuntimeConfig): string => { + const raw = config.extraConfig?.site; + if (typeof raw !== "string" || raw.length === 0) return "datadoghq.com"; + return raw + .trim() + .replace(/^https?:\/\//, "") + .replace(/\/+$/, ""); +}; + +export const datadogAdapter: LogProviderAdapter = { + type: "datadog", + label: "Datadog Logs", + docsUrl: "https://docs.datadoghq.com/logs/", + credentialFields: [ + { + key: "apiKey", + label: "API Key", + type: "password", + required: true, + fullWidth: true, + }, + { + key: "site", + label: "Site", + type: "text", + required: false, + placeholder: "datadoghq.com", + helpText: "E.g. datadoghq.com (US1) or datadoghq.eu (EU).", + fullWidth: true, + }, + ], + toVectorTransform( + _config: LogProviderRuntimeConfig, + _transformId: string, + scopeTransformId: string, + ): VectorTransformConfig { + return { + type: "remap", + inputs: [scopeTransformId], + source: DDTAGS_VRL, + }; + }, + toVectorSink( + config: LogProviderRuntimeConfig, + _sinkId: string, + inputId: string, + ): VectorSinkConfig { + return { + type: "datadog_logs", + inputs: [inputId], + default_api_key: config.apiKey ?? "", + site: resolveSite(config), + buffer: DEFAULT_DISK_BUFFER, + }; + }, + async testConnection(config: LogProviderRuntimeConfig): Promise { + if (!config.apiKey) { + throw new Error("Datadog API key is required"); + } + const site = resolveSite(config); + const response = await logProviderFetch( + `https://api.${site}/api/v1/validate`, + { + headers: { "DD-API-KEY": config.apiKey }, + }, + ); + if (!response.ok) { + throw new Error( + `Datadog API key validation failed with status ${response.status}`, + ); + } + }, +}; diff --git a/packages/server/src/services/log-management/providers/elasticsearch.ts b/packages/server/src/services/log-management/providers/elasticsearch.ts new file mode 100644 index 000000000..274c14ec7 --- /dev/null +++ b/packages/server/src/services/log-management/providers/elasticsearch.ts @@ -0,0 +1,153 @@ +import type { + LogProviderAdapter, + LogProviderRuntimeConfig, + VectorSinkConfig, + VectorTransformConfig, +} from "../types"; +import { + DEFAULT_DISK_BUFFER, + logProviderFetch, + normalizeEndpointUrl, +} from "../types"; + +const LABEL_TO_JSON_VRL = ` +if exists(.label) { .label = encode_json(.label) } +`.trim(); + +type AuthMode = + | { kind: "basic"; username: string; password: string } + | { kind: "apiKey"; apiKey: string } + | { kind: "none" }; + +const resolveAuthMode = (config: LogProviderRuntimeConfig): AuthMode => { + const username = config.extraConfig?.username; + const hasUsername = typeof username === "string" && username.length > 0; + if (hasUsername && !config.apiKey) { + throw new Error( + "A Password / API Key is required when Username is set (leave both blank for no auth).", + ); + } + if (hasUsername && config.apiKey) { + return { + kind: "basic", + username: username as string, + password: config.apiKey, + }; + } + if (config.apiKey) { + return { kind: "apiKey", apiKey: config.apiKey }; + } + return { kind: "none" }; +}; + +const buildAuthAndHeaders = ( + config: LogProviderRuntimeConfig, +): Record => { + const mode = resolveAuthMode(config); + if (mode.kind === "basic") { + return { + auth: { strategy: "basic", user: mode.username, password: mode.password }, + }; + } + if (mode.kind === "apiKey") { + return { + request: { headers: { Authorization: `ApiKey ${mode.apiKey}` } }, + }; + } + return {}; +}; + +export const elasticsearchAdapter: LogProviderAdapter = { + type: "elasticsearch", + label: "Elasticsearch / OpenSearch", + docsUrl: + "https://vector.dev/docs/reference/configuration/sinks/elasticsearch/", + credentialFields: [ + { + key: "endpoint", + label: "Endpoint", + type: "url", + required: true, + placeholder: "https://elasticsearch.example.com:9200", + }, + { + key: "username", + label: "Username", + type: "text", + required: false, + helpText: "Leave blank to use an API key instead of basic auth.", + }, + { + key: "apiKey", + label: "Password / API Key", + type: "password", + required: false, + helpText: + "Password for Username, or an API key alone. Leave both blank for no auth.", + fullWidth: true, + }, + { + key: "index", + label: "Index", + type: "text", + required: false, + placeholder: "vector-%Y.%m.%d", + helpText: "Defaults to vector-%Y.%m.%d, a daily index.", + fullWidth: true, + }, + ], + toVectorTransform( + _config: LogProviderRuntimeConfig, + _transformId: string, + scopeTransformId: string, + ): VectorTransformConfig { + return { + type: "remap", + inputs: [scopeTransformId], + source: LABEL_TO_JSON_VRL, + }; + }, + toVectorSink( + config: LogProviderRuntimeConfig, + _sinkId: string, + inputId: string, + ): VectorSinkConfig { + const index = config.extraConfig?.index; + return { + type: "elasticsearch", + inputs: [inputId], + endpoints: [normalizeEndpointUrl(config.endpoint ?? "")], + ...(typeof index === "string" && index.length > 0 + ? { bulk: { index } } + : {}), + ...buildAuthAndHeaders(config), + buffer: DEFAULT_DISK_BUFFER, + }; + }, + async testConnection(config: LogProviderRuntimeConfig): Promise { + if (!config.endpoint) { + throw new Error("Elasticsearch/OpenSearch endpoint is required"); + } + const mode = resolveAuthMode(config); + const headers: Record = {}; + if (mode.kind === "basic") { + headers.Authorization = `Basic ${Buffer.from( + `${mode.username}:${mode.password}`, + ).toString("base64")}`; + } else if (mode.kind === "apiKey") { + headers.Authorization = `ApiKey ${mode.apiKey}`; + } + const response = await logProviderFetch( + `${normalizeEndpointUrl(config.endpoint).replace(/\/$/, "")}/_cluster/health`, + { headers }, + ); + if (!response.ok) { + throw new Error( + `Elasticsearch/OpenSearch health check failed with status ${response.status}`, + ); + } + }, + validateConfig(config: LogProviderRuntimeConfig): void { + resolveAuthMode(config); + }, +}; diff --git a/packages/server/src/services/log-management/providers/loki.ts b/packages/server/src/services/log-management/providers/loki.ts new file mode 100644 index 000000000..202298acb --- /dev/null +++ b/packages/server/src/services/log-management/providers/loki.ts @@ -0,0 +1,119 @@ +import type { + LogProviderAdapter, + LogProviderRuntimeConfig, + VectorSinkConfig, +} from "../types"; +import { + DEFAULT_DISK_BUFFER, + logProviderFetch, + normalizeEndpointUrl, +} from "../types"; + +export const lokiAdapter: LogProviderAdapter = { + type: "loki", + label: "Grafana Loki", + docsUrl: "https://grafana.com/docs/loki/latest/", + credentialFields: [ + { + key: "endpoint", + label: "Endpoint", + type: "url", + required: true, + placeholder: "https://loki.example.com", + helpText: "Base URL of your Loki instance (push API).", + }, + { + key: "username", + label: "User / Instance ID", + type: "text", + required: false, + helpText: + "Leave blank for an unauthenticated Loki. For Grafana Cloud, this is your numeric Instance ID.", + }, + { + key: "apiKey", + label: "Password / API Token", + type: "password", + required: false, + helpText: + "Leave blank for an unauthenticated Loki. For Grafana Cloud, use an API token here.", + }, + { + key: "tenantId", + label: "Tenant ID", + type: "text", + required: false, + helpText: "Only if your Loki runs in multi-tenant mode.", + fullWidth: true, + }, + ], + toVectorSink( + config: LogProviderRuntimeConfig, + _sinkId: string, + inputId: string, + ): VectorSinkConfig { + const tenantId = config.extraConfig?.tenantId; + const username = config.extraConfig?.username; + return { + type: "loki", + inputs: [inputId], + endpoint: normalizeEndpointUrl(config.endpoint ?? ""), + encoding: { codec: "json" }, + labels: { + dokploy_project: "{{ dokploy_project }}", + dokploy_application: "{{ dokploy_application }}", + dokploy_organization: "{{ dokploy_organization }}", + }, + ...(typeof tenantId === "string" && tenantId.length > 0 + ? { tenant_id: tenantId } + : {}), + ...(config.apiKey + ? { + auth: { + strategy: "basic", + user: typeof username === "string" ? username : "", + password: config.apiKey, + }, + } + : {}), + buffer: DEFAULT_DISK_BUFFER, + }; + }, + async testConnection(config: LogProviderRuntimeConfig): Promise { + if (!config.endpoint) { + throw new Error("Loki endpoint is required"); + } + const username = config.extraConfig?.username; + const tenantId = config.extraConfig?.tenantId; + const response = await logProviderFetch( + `${normalizeEndpointUrl(config.endpoint).replace(/\/$/, "")}/loki/api/v1/push`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + ...(config.apiKey + ? { + Authorization: `Basic ${Buffer.from( + `${typeof username === "string" ? username : ""}:${config.apiKey}`, + ).toString("base64")}`, + } + : {}), + ...(typeof tenantId === "string" && tenantId.length > 0 + ? { "X-Scope-OrgID": tenantId } + : {}), + }, + body: JSON.stringify({ + streams: [ + { + stream: { dokploy_test: "true" }, + values: [[`${Date.now()}000000`, "dokploy-log-provider-test"]], + }, + ], + }), + }, + ); + if (!response.ok) { + throw new Error(`Loki push test failed with status ${response.status}`); + } + }, +}; diff --git a/packages/server/src/services/log-management/providers/registry.ts b/packages/server/src/services/log-management/providers/registry.ts new file mode 100644 index 000000000..ea97847b4 --- /dev/null +++ b/packages/server/src/services/log-management/providers/registry.ts @@ -0,0 +1,27 @@ +import type { LogProviderAdapter, LogProviderType } from "../types"; +import { awsCloudwatchAdapter } from "./aws-cloudwatch"; +import { betterStackAdapter } from "./betterstack"; +import { datadogAdapter } from "./datadog"; +import { elasticsearchAdapter } from "./elasticsearch"; +import { lokiAdapter } from "./loki"; +import { splunkAdapter } from "./splunk"; + +export const logProviderAdapters: Record = + { + loki: lokiAdapter, + datadog: datadogAdapter, + betterstack: betterStackAdapter, + elasticsearch: elasticsearchAdapter, + splunk_hec: splunkAdapter, + aws_cloudwatch: awsCloudwatchAdapter, + }; + +export function getLogProviderAdapter( + type: LogProviderType, +): LogProviderAdapter { + const adapter = logProviderAdapters[type]; + if (!adapter) { + throw new Error(`No LogProviderAdapter registered for type "${type}"`); + } + return adapter; +} diff --git a/packages/server/src/services/log-management/providers/splunk.ts b/packages/server/src/services/log-management/providers/splunk.ts new file mode 100644 index 000000000..b21f6cdd3 --- /dev/null +++ b/packages/server/src/services/log-management/providers/splunk.ts @@ -0,0 +1,80 @@ +import type { + LogProviderAdapter, + LogProviderRuntimeConfig, + VectorSinkConfig, +} from "../types"; +import { + DEFAULT_DISK_BUFFER, + logProviderFetch, + normalizeEndpointUrl, +} from "../types"; + +export const splunkAdapter: LogProviderAdapter = { + type: "splunk_hec", + label: "Splunk HTTP Event Collector", + docsUrl: + "https://vector.dev/docs/reference/configuration/sinks/splunk_hec_logs/", + credentialFields: [ + { + key: "endpoint", + label: "Endpoint", + type: "url", + required: true, + placeholder: "https://splunk.example.com:8088", + helpText: "Base URL of your Splunk instance, including the HEC port.", + }, + { + key: "apiKey", + label: "HEC Token", + type: "password", + required: true, + }, + { + key: "index", + label: "Index", + type: "text", + required: false, + }, + { + key: "sourcetype", + label: "Source Type", + type: "text", + required: false, + placeholder: "httpevent", + }, + ], + toVectorSink( + config: LogProviderRuntimeConfig, + _sinkId: string, + inputId: string, + ): VectorSinkConfig { + const index = config.extraConfig?.index; + const sourcetype = config.extraConfig?.sourcetype; + return { + type: "splunk_hec_logs", + inputs: [inputId], + endpoint: normalizeEndpointUrl(config.endpoint ?? ""), + default_token: config.apiKey ?? "", + encoding: { codec: "json" }, + ...(typeof index === "string" && index.length > 0 ? { index } : {}), + ...(typeof sourcetype === "string" && sourcetype.length > 0 + ? { sourcetype } + : {}), + buffer: DEFAULT_DISK_BUFFER, + }; + }, + async testConnection(config: LogProviderRuntimeConfig): Promise { + if (!config.endpoint || !config.apiKey) { + throw new Error("Splunk endpoint and HEC token are required"); + } + const response = await logProviderFetch( + `${normalizeEndpointUrl(config.endpoint).replace(/\/$/, "")}/services/collector/health`, + { headers: { Authorization: `Splunk ${config.apiKey}` } }, + ); + if (!response.ok) { + throw new Error( + `Splunk HEC health check failed with status ${response.status}`, + ); + } + }, +}; diff --git a/packages/server/src/services/log-management/service.ts b/packages/server/src/services/log-management/service.ts new file mode 100644 index 000000000..31467185f --- /dev/null +++ b/packages/server/src/services/log-management/service.ts @@ -0,0 +1,269 @@ +import { db } from "@dokploy/server/db"; +import { + type apiCreateLogProvider, + logProvider, +} from "@dokploy/server/db/schema"; +import { TRPCError } from "@trpc/server"; +import { and, eq } from "drizzle-orm"; +import type { z } from "zod"; +import { getLogProviderAdapter } from "./providers/registry"; +import type { LogProviderRuntimeConfig, LogProviderType } from "./types"; + +export type LogProvider = typeof logProvider.$inferSelect; + +const CREDENTIAL_COLUMNS = { + endpoint: false, + apiKey: false, + apiSecret: false, +} as const; + +const assertRequiredCredentialFields = ( + providerType: LogProviderType, + values: { + endpoint?: string | null; + apiKey?: string | null; + apiSecret?: string | null; + extraConfig?: Record | null; + }, +) => { + const adapter = getLogProviderAdapter(providerType); + const fieldValues: Record = { + ...(values.extraConfig ?? {}), + endpoint: values.endpoint, + apiKey: values.apiKey, + apiSecret: values.apiSecret, + }; + const missing = adapter.credentialFields + .filter((field) => field.required) + .filter((field) => { + const value = fieldValues[field.key]; + return value === undefined || value === null || value === ""; + }) + .map((field) => field.label); + + if (missing.length > 0) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: `Missing required field(s) for ${adapter.label}: ${missing.join(", ")}`, + }); + } + + if (adapter.validateConfig) { + try { + adapter.validateConfig({ + logProviderId: "validate", + name: "", + endpoint: values.endpoint ?? null, + apiKey: values.apiKey ?? null, + apiSecret: values.apiSecret ?? null, + extraConfig: values.extraConfig ?? null, + }); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: + error instanceof Error ? error.message : "Invalid configuration", + }); + } + } +}; + +export const createLogProvider = async ( + input: z.infer, + organizationId: string, +) => { + assertRequiredCredentialFields(input.providerType, input); + + const created = await db + .insert(logProvider) + .values({ + ...input, + organizationId, + }) + .returning() + .then((rows) => rows[0]); + + if (!created) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error creating log provider", + }); + } + return created; +}; + +export const updateLogProvider = async ( + logProviderId: string, + data: Partial>, +) => { + const existing = await findLogProviderByIdWithCredentials(logProviderId); + const isChangingType = + data.providerType !== undefined && + data.providerType !== existing.providerType; + const dataToApply = isChangingType + ? { + endpoint: null, + apiKey: null, + apiSecret: null, + extraConfig: null, + ...data, + } + : data.extraConfig != null + ? { + ...data, + extraConfig: { ...(existing.extraConfig ?? {}), ...data.extraConfig }, + } + : data; + const merged = { ...existing, ...dataToApply }; + assertRequiredCredentialFields(merged.providerType, merged); + + const updated = await db + .update(logProvider) + .set(dataToApply) + .where(eq(logProvider.logProviderId, logProviderId)) + .returning() + .then((rows) => rows[0]); + + if (!updated) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Log provider not found", + }); + } + return updated; +}; + +export const removeLogProvider = async (logProviderId: string) => { + const removed = await db + .delete(logProvider) + .where(eq(logProvider.logProviderId, logProviderId)) + .returning() + .then((rows) => rows[0]); + + if (!removed) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Log provider not found", + }); + } + return removed; +}; + +export const findLogProviderById = async (logProviderId: string) => { + const found = await db.query.logProvider.findFirst({ + where: eq(logProvider.logProviderId, logProviderId), + columns: CREDENTIAL_COLUMNS, + }); + if (!found) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Log provider not found", + }); + } + return found; +}; + +export const findLogProviderByIdWithCredentials = async ( + logProviderId: string, +) => { + const found = await db.query.logProvider.findFirst({ + where: eq(logProvider.logProviderId, logProviderId), + }); + if (!found) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Log provider not found", + }); + } + return found; +}; + +export const findLogProvidersByOrganization = async ( + organizationId: string, +) => { + return await db.query.logProvider.findMany({ + where: eq(logProvider.organizationId, organizationId), + columns: CREDENTIAL_COLUMNS, + }); +}; + +export const findEnabledLogProvidersByOrganization = async ( + organizationId: string, +) => { + return await db.query.logProvider.findMany({ + where: and( + eq(logProvider.organizationId, organizationId), + eq(logProvider.enabled, true), + ), + }); +}; + +export const hasEnabledLogProvider = async (organizationId: string) => { + const found = await db.query.logProvider.findFirst({ + where: and( + eq(logProvider.organizationId, organizationId), + eq(logProvider.enabled, true), + ), + columns: { logProviderId: true }, + }); + return !!found; +}; + +export const toRuntimeConfig = ( + provider: LogProvider, +): LogProviderRuntimeConfig => ({ + logProviderId: provider.logProviderId, + name: provider.name, + endpoint: provider.endpoint, + apiKey: provider.apiKey, + apiSecret: provider.apiSecret, + extraConfig: provider.extraConfig, +}); + +export const testLogProviderConnection = async ( + params: + | { logProviderId: string } + | { providerType: LogProviderType; config: LogProviderRuntimeConfig }, +) => { + let providerType: LogProviderType; + let runtimeConfig: LogProviderRuntimeConfig; + + if ("logProviderId" in params) { + const provider = await findLogProviderByIdWithCredentials( + params.logProviderId, + ); + providerType = provider.providerType; + runtimeConfig = toRuntimeConfig(provider); + } else { + providerType = params.providerType; + runtimeConfig = params.config; + } + + const adapter = getLogProviderAdapter(providerType); + if (!adapter.testConnection) { + return { + success: true, + warning: "This provider does not support connection testing", + }; + } + + try { + await adapter.testConnection(runtimeConfig); + return { success: true }; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: + error instanceof Error ? error.message : "Connection test failed", + }); + } +}; + +export const sanitizeLogProvider = < + T extends { endpoint?: unknown; apiKey?: unknown; apiSecret?: unknown }, +>( + provider: T, +) => { + const { endpoint, apiKey, apiSecret, ...rest } = provider; + return rest; +}; diff --git a/packages/server/src/services/log-management/types.ts b/packages/server/src/services/log-management/types.ts new file mode 100644 index 000000000..a55c5705d --- /dev/null +++ b/packages/server/src/services/log-management/types.ts @@ -0,0 +1,124 @@ +export const LOG_PROVIDER_REQUEST_TIMEOUT_MS = 15_000; + +const isMetadataAddress = (address: string): boolean => { + const lower = address.toLowerCase().replace(/^\[|\]$/g, ""); + if (lower === "fd00:ec2::254") return true; + if (lower.startsWith("169.254.")) return true; + if (/^fe[89ab]/.test(lower)) return true; + return false; +}; + +const METADATA_HOSTNAMES = new Set([ + "metadata.google.internal", + "metadata.goog", +]); + +const assertNotMetadataEndpoint = async (rawUrl: string): Promise => { + let url: URL; + try { + url = new URL(rawUrl); + } catch { + return; + } + const blocked = () => + new Error( + "This endpoint resolves to a cloud metadata address and can't be used here.", + ); + if (METADATA_HOSTNAMES.has(url.hostname.toLowerCase())) { + throw blocked(); + } + if (isMetadataAddress(url.hostname)) { + throw blocked(); + } + try { + const { lookup } = await import("node:dns/promises"); + const results = await lookup(url.hostname, { all: true }); + if (results.some((r) => isMetadataAddress(r.address))) { + throw blocked(); + } + } catch (error) { + if (error instanceof Error && error.message.includes("metadata")) { + throw error; + } + } +}; + +export const logProviderFetch = async (url: string, init: RequestInit = {}) => { + await assertNotMetadataEndpoint(url); + return await fetch(url, { + ...init, + signal: AbortSignal.timeout(LOG_PROVIDER_REQUEST_TIMEOUT_MS), + }); +}; + +export interface LogProviderCredentialField { + key: string; + label: string; + type: "text" | "password" | "url"; + required: boolean; + placeholder?: string; + helpText?: string; + fullWidth?: boolean; +} + +export interface VectorTransformConfig { + type: string; + inputs: string[]; + [key: string]: unknown; +} + +export interface VectorSinkConfig { + type: string; + inputs: string[]; + buffer: { + type: "disk" | "memory"; + max_size: number; + when_full?: "block" | "drop_newest"; + }; + [key: string]: unknown; +} + +export interface LogProviderRuntimeConfig { + logProviderId: string; + name: string; + endpoint: string | null; + apiKey: string | null; + apiSecret: string | null; + extraConfig: Record | null; +} + +export type LogProviderType = + | "loki" + | "datadog" + | "betterstack" + | "elasticsearch" + | "splunk_hec" + | "aws_cloudwatch"; + +export const DEFAULT_DISK_BUFFER = { + type: "disk" as const, + max_size: 268_435_488, + when_full: "block" as const, +}; + +export const normalizeEndpointUrl = (endpoint: string): string => + !endpoint || /^https?:\/\//.test(endpoint) ? endpoint : `https://${endpoint}`; + +export interface LogProviderAdapter { + type: LogProviderType; + label: string; + docsUrl?: string; + credentialFields: LogProviderCredentialField[]; + toVectorTransform?( + config: LogProviderRuntimeConfig, + transformId: string, + scopeTransformId: string, + ): VectorTransformConfig; + toVectorSink( + config: LogProviderRuntimeConfig, + sinkId: string, + inputId: string, + ): VectorSinkConfig; + testConnection?(config: LogProviderRuntimeConfig): Promise; + validateConfig?(config: LogProviderRuntimeConfig): void; +} diff --git a/packages/server/src/services/server.ts b/packages/server/src/services/server.ts index aee623483..a7c56c5c3 100644 --- a/packages/server/src/services/server.ts +++ b/packages/server/src/services/server.ts @@ -87,6 +87,25 @@ 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), + eq(server.enableLogManagement, true), + ), + columns: { serverId: true }, + }); +}; + +export const findAllServersWithLogManagementEnabled = async () => { + return await db.query.server.findMany({ + where: eq(server.enableLogManagement, true), + columns: { serverId: true, organizationId: true }, + }); +}; + export const deleteServer = async (serverId: string) => { const currentServer = await db .delete(server) diff --git a/packages/server/src/services/web-server-settings.ts b/packages/server/src/services/web-server-settings.ts index 289d119c9..bce77983b 100644 --- a/packages/server/src/services/web-server-settings.ts +++ b/packages/server/src/services/web-server-settings.ts @@ -1,6 +1,6 @@ import { db } from "@dokploy/server/db"; import { webServerSettings } from "@dokploy/server/db/schema"; -import { eq } from "drizzle-orm"; +import { and, eq, isNull, or } from "drizzle-orm"; /** * Get the web server settings (singleton - only one row should exist) @@ -42,3 +42,33 @@ export const updateWebServerSettings = async ( return updated; }; + +export const claimWebServerLogManagement = async ( + organizationId: string, + enableLogManagement: boolean, +) => { + const current = await getWebServerSettings(); + if (!current) { + return null; + } + + const [updated] = await db + .update(webServerSettings) + .set({ + enableLogManagement, + logManagementOrganizationId: enableLogManagement ? organizationId : null, + updatedAt: new Date(), + }) + .where( + and( + eq(webServerSettings.id, current.id), + or( + isNull(webServerSettings.logManagementOrganizationId), + eq(webServerSettings.logManagementOrganizationId, organizationId), + ), + ), + ) + .returning(); + + return updated ?? null; +}; diff --git a/packages/server/src/setup/vector-setup.ts b/packages/server/src/setup/vector-setup.ts new file mode 100644 index 000000000..55effe56a --- /dev/null +++ b/packages/server/src/setup/vector-setup.ts @@ -0,0 +1,656 @@ +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 { + findEnabledLogProvidersByOrganization, + hasEnabledLogProvider, + type LogProvider, + toRuntimeConfig, +} from "@dokploy/server/services/log-management/service"; +import { findServerById } from "@dokploy/server/services/server"; +import { getWebServerSettings } from "@dokploy/server/services/web-server-settings"; +import { pullImage, pullRemoteImage } 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 { stringify } from "yaml"; +import { getLogProviderAdapter } from "../services/log-management/providers/registry"; +import type { + VectorSinkConfig, + VectorTransformConfig, +} from "../services/log-management/types"; + +const VECTOR_IMAGE = "timberio/vector:latest-alpine"; +const VECTOR_SERVICE_NAME = "dokploy-vector"; +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> => { + 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 = {}; + 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 => { + 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 buildSinksAndTransforms = ( + providers: Array, + baseTransformId: string, +): { + sinks: Record; + transforms: Record; +} => { + const sinks: Record = {}; + const transforms: Record = {}; + + for (const provider of providers) { + try { + const adapter = getLogProviderAdapter(provider.providerType); + const runtimeConfig = toRuntimeConfig(provider); + const sinkId = `sink_${provider.logProviderId}`; + let inputId = baseTransformId; + let transform: { id: string; config: VectorTransformConfig } | null = + null; + if (adapter.toVectorTransform) { + const transformId = `transform_${provider.logProviderId}`; + transform = { + id: transformId, + config: adapter.toVectorTransform( + runtimeConfig, + transformId, + baseTransformId, + ), + }; + inputId = transformId; + } + const sink = adapter.toVectorSink(runtimeConfig, sinkId, inputId); + if (transform) { + transforms[transform.id] = transform.config; + } + sinks[sinkId] = sink; + } catch (error) { + console.error( + `[Vector] Skipping log provider "${provider.name}" (${provider.logProviderId}) — invalid config:`, + error, + ); + } + } + + return { sinks, transforms }; +}; + +export interface VectorOrgData { + providers: Array; + lookup: Record; +} + +export const loadVectorOrgData = async ( + organizationId: string, +): Promise => { + const [providers, lookup] = await Promise.all([ + findEnabledLogProvidersByOrganization(organizationId), + buildAppNameLookup(organizationId), + ]); + return { providers, lookup }; +}; + +export const buildVectorConfigYaml = async ( + organizationId: string, + preloaded?: VectorOrgData, + options?: { dropUnmatched?: boolean }, +) => { + const { providers, lookup } = + preloaded ?? (await loadVectorOrgData(organizationId)); + const baseTransformId = options?.dropUnmatched + ? "dokploy_scope_local_only" + : "dokploy_scope"; + const { sinks, transforms } = buildSinksAndTransforms( + providers, + baseTransformId, + ); + + const config = { + data_dir: VECTOR_DATA_DIR_CONTAINER, + sources: { + docker_logs_source: { + type: "docker_logs", + docker_host: "unix:///var/run/docker.sock", + }, + }, + transforms: { + dokploy_scope: { + type: "remap", + inputs: ["docker_logs_source"], + source: buildScopeTransformSource(organizationId, lookup), + }, + ...(options?.dropUnmatched + ? { + dokploy_scope_local_only: { + type: "filter", + inputs: ["dokploy_scope"], + condition: '.dokploy_project != ""', + }, + } + : {}), + ...transforms, + }, + sinks, + }; + + return stringify(config); +}; + +export const collectSecretValues = ( + providers: Array, +): string[] => { + const values: string[] = []; + for (const provider of providers) { + if (provider.endpoint) values.push(provider.endpoint); + if (provider.apiKey) values.push(provider.apiKey); + if (provider.apiSecret) values.push(provider.apiSecret); + for (const value of Object.values(provider.extraConfig ?? {})) { + if (typeof value === "string" && value.length > 0) values.push(value); + } + } + return values.filter((v) => v.length > 0).sort((a, b) => b.length - a.length); +}; + +export const redactSecrets = (text: string, secrets: string[]): string => { + let redacted = text; + for (const secret of secrets) { + redacted = redacted.split(secret).join("[redacted]"); + } + 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 | 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>(); + +export const withConfigWriteLock = async ( + serverId: string, + fn: () => Promise, +): Promise => { + const previous = configWriteLocks.get(serverId) ?? Promise.resolve(); + let release: () => void = () => {}; + const current = new Promise((resolve) => { + release = resolve; + }); + configWriteLocks.set( + serverId, + previous.then(() => current), + ); + await previous; + try { + return await runWithCrossProcessLock(serverId, fn); + } finally { + release(); + } +}; + +const runWithCrossProcessLock = async ( + serverId: string, + fn: () => Promise, +): Promise => { + 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 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); + + 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}`); + }); + +export const buildServiceSettings = ( + vectorPath: string, +): CreateServiceOptions => { + const VECTOR_PATH = vectorPath; + return { + Name: VECTOR_SERVICE_NAME, + TaskTemplate: { + ContainerSpec: { + Image: VECTOR_IMAGE, + Args: ["--config", VECTOR_CONFIG_CONTAINER_PATH, "--watch-config"], + Mounts: [ + { + Type: "bind", + Source: "/var/run/docker.sock", + Target: "/var/run/docker.sock", + ReadOnly: true, + }, + { + Type: "bind", + Source: VECTOR_PATH, + Target: VECTOR_CONFIG_CONTAINER_DIR, + ReadOnly: true, + }, + { + Type: "bind", + Source: `${VECTOR_PATH}/data`, + Target: VECTOR_DATA_DIR_CONTAINER, + ReadOnly: false, + }, + ], + }, + Networks: [{ Target: "host" }], + }, + Mode: { + Replicated: { + Replicas: 1, + }, + }, + }; +}; + +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>, + 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, + TaskTemplate: { + ...settings.TaskTemplate, + 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); + 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, + ); + } + throw new Error( + `Generated Vector config failed validation, not applying it: ${sanitizedDetail}`, + ); + } + 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(); + try { + await docker.getService(VECTOR_SERVICE_NAME).remove(); + } catch (error: any) { + if (error?.statusCode !== 404) { + 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); + } else { + await removeWebVectorAgent(); + } + + return { installed: shouldRun }; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c3b37ef6b..5d0630f0f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -117,6 +117,9 @@ importers: '@ai-sdk/openai-compatible': specifier: ^2.0.30 version: 2.0.30(zod@4.3.6) + '@aws-sdk/client-cloudwatch-logs': + specifier: ^3.1108.0 + version: 3.1127.0 '@aws-sdk/client-route-53': specifier: ^3.1108.0 version: 3.1108.0 @@ -596,6 +599,9 @@ importers: '@ai-sdk/openai-compatible': specifier: ^2.0.30 version: 2.0.30(zod@4.3.6) + '@aws-sdk/client-cloudwatch-logs': + specifier: ^3.1108.0 + version: 3.1127.0 '@aws-sdk/client-route-53': specifier: ^3.1108.0 version: 3.1108.0 @@ -894,6 +900,10 @@ packages: resolution: {integrity: sha512-cTlrKttbrRHEw3W+0/I609A2Matj5JQaRvfLtEIGZvlN0RaPi+3ANsMeqAyCAVlH/lUIW2tmtBlSMni74lcXeg==} engines: {node: '>=12'} + '@aws-sdk/client-cloudwatch-logs@3.1127.0': + resolution: {integrity: sha512-BhRRVPLSEAQ3Yep8izRyVaMfHk1inNHm1C4txBWoJQYDVE/EX/SAIxtnpaF9KZYZzgBn8l7CBXC616435E2vlw==} + engines: {node: '>=20.0.0'} + '@aws-sdk/client-route-53@3.1108.0': resolution: {integrity: sha512-fbHGq25KlbqSIEQC3T+3ghsXmnpknjU3VxMgTOmqbKAqTLVB0ZLhxzPOxKuJFTd5m5mzSh9lCAZR5Nnlg+V9kQ==} engines: {node: '>=20.0.0'} @@ -910,38 +920,74 @@ packages: resolution: {integrity: sha512-I88Iov89NVmjSmJLKSv7Cn9M2J+a2942OkA8nZCbz+sl4ZeY4zEOcoLOrbt1GRfQ8zEQKnjAJdXixA3J/p1fDQ==} engines: {node: '>=20.0.0'} + '@aws-sdk/core@3.977.9': + resolution: {integrity: sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-env@3.972.68': resolution: {integrity: sha512-2a20A/IdNOwUvaDq91iqqS7BA0XlNMfW3iLGZGZLJv0EbUqhSxB0PIx4rQQqssvWj1uXImb3/UCCdHz/+1dOiA==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-env@3.972.70': + resolution: {integrity: sha512-H404B7dJl2mCrBqahDEYsanB0xhdDp6tXnXcTUnXmmpy2Q3J0Ho0bUajZ2jr/RdwzCyS59Gi8xXIFwPLGBl6Uw==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-http@3.972.70': resolution: {integrity: sha512-0yRem2Fs52r/Nn6UAqIlpjexfaYj8ziEozOe9tamtAVT/5bzFLKx8O2r7MaRqgS3hGKHIa1Jij9nKHSsNnb04A==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-http@3.972.72': + resolution: {integrity: sha512-X98zYOrVOeuosCX+6ktf29FC2N2GHPLia7qv6mzPzTc+RPAuHWCDS++Z6JK7eGYqb/v6uaW7bAXaOvDBfol+0w==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-ini@3.973.13': resolution: {integrity: sha512-2M39DE02XpYYaSWYk/4AsImXYUU/1L2xmTMLUpMMWq7DfLv191/vCRy3baKtdr45AkJQyVgSjmuVOLm15SwrRQ==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-ini@3.973.15': + resolution: {integrity: sha512-Rykg6s5ceBuynMOGWgoowO4N+27JfnqXAnVaSunZl0hOO1XodSrxGNz6sCEbnmS0lAfQZDKyb3fbr46gSuv6Sg==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-login@3.972.75': resolution: {integrity: sha512-jaTESuJlQsoUZ44f/i2puyPt8VlF/dMMJ9HM3cStYtk7eKX4N9UWi83OLixUkoOJH3BwWlPLCq9YIK9nfWhVBg==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-login@3.972.77': + resolution: {integrity: sha512-Jb59xfEISoN5mmbnA+HYqdtrSX3CgCtJoof+V5D8/TgUI56W63GEEd5Y58WijU3Ou6+WEgaLD1feVzaRXV5IDQ==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-node@3.972.79': resolution: {integrity: sha512-RIw5dof1EHkWubrZzPC941CDtnFG1iAXsxbFgLkhdYZXHc4icU13c/uxSMI0J5eUx9bxa7LjfpdjfClBB1QsDA==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-node@3.972.82': + resolution: {integrity: sha512-znDkEOGXB8W3kG1LJUKP3foBZY/9qLM0eil/DxWXSp37XsdsRLQHE/d/OaCGGVgKpA6znR38h/+INk8do1FjiA==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-process@3.972.68': resolution: {integrity: sha512-nLP3Pda2MQTFJ25hKBMmUuB9Uv+bTZQNlufbeCwklP549Vwnkd8bRLJoCKp5k6xjmdyptrPrOfGOhN0mKuca8A==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-process@3.972.70': + resolution: {integrity: sha512-2ry03fGRJr4sV3jI+ocjj5JqALnFD6ymM5KiNCDZMvq8bX2GSbE0vji4aM43TVCl2nXqqLRZaUxdq/KeWRAY4Q==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-sso@3.973.12': resolution: {integrity: sha512-EmgyyHn+f9WCcelp3L/vci+LGbX8GigWaVphRArjVo5Pktkr9YnLy/mQ6VDkDyBD72dtfRNTgHmD2ts4rTDXKQ==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-sso@3.973.14': + resolution: {integrity: sha512-jkhg/8ocAAoc0RFyLMhCw+/zZh7gystQgd4F4hznNa8P4Cc501PQmxd+jGLiMHodPJ+7Zv/3znM62gZojyasmA==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-web-identity@3.972.74': resolution: {integrity: sha512-0YfczxGXF3RjGj8z7QG/Ho2HnLGKDHfPSHiTs47UU1U/+mmwISDN+rvGKt2zh+3FX8NdT4xd95LGBGyhQw2dgQ==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-web-identity@3.972.76': + resolution: {integrity: sha512-d3AGyVu759PGr35mEB2s22xxlNEA5rpdxtSPJthfPFJvoQ8dt357iVPECqWfUxXp1toJAvKmbtcIYVGigaGsCA==} + engines: {node: '>=20.0.0'} + '@aws-sdk/middleware-sdk-route53@3.972.24': resolution: {integrity: sha512-+z3OqGhqLA46vF5KbnjuygzSQo9A2cWsbuDy1rdrbWSPDCNT5CCRC+veIxRnqvxwpj/BY8P8XNz+WwCmX0e8rg==} engines: {node: '>=20.0.0'} @@ -950,22 +996,42 @@ packages: resolution: {integrity: sha512-XWRyon2MTHXD/zMoo0Mbge6Vwf+iE0qQaM/RyGO6NfZ9WukCFiQL27nQVZjYy2JwSIg+iXZxKOX95OBXqlSM4w==} engines: {node: '>=20.0.0'} + '@aws-sdk/nested-clients@3.997.44': + resolution: {integrity: sha512-NhEgryjlBF9w38ZXqGymQV28IhkYa1mKhlbYnqIis57AYwWGVYfUPgg/qC2rLRqOUfblxx++irvju10kVTa8Vw==} + engines: {node: '>=20.0.0'} + '@aws-sdk/signature-v4-multi-region@3.996.44': resolution: {integrity: sha512-ZSfQ35Qn4MhSY+A0Whyr+KBx+wJKZUyBsOrjB2pSHOafRzbFe47T8XcXM8hZqUAC69qnqIy0C9ArxTuud0CC2w==} engines: {node: '>=20.0.0'} + '@aws-sdk/signature-v4-multi-region@3.996.46': + resolution: {integrity: sha512-L+2xZTye/2T96f3lwCws0Zw6GG2JHZW9e8FpVgGBeeExSKyeoZ6CWRpBml/7DNiK/O26jrgPM9F+Ay8VkgzUWQ==} + engines: {node: '>=20.0.0'} + '@aws-sdk/token-providers@3.1108.0': resolution: {integrity: sha512-rI80zxDxGJ6904eC/YbjkdjY6JdaZvQ01kOmrMvw7cFQGIHo27fhnIVbMSVDS4T6foQImjxYSRoOu/uSJscXDw==} engines: {node: '>=20.0.0'} + '@aws-sdk/token-providers@3.1116.0': + resolution: {integrity: sha512-ygIivKqh8aHzNkucOCXHyIBgBpLPfrSI0mCqXF+vLBsPTUKqj0VSqAY0GFPe7lQl4HntjOcQ+KSyS7oUV2C54Q==} + engines: {node: '>=20.0.0'} + '@aws-sdk/types@3.974.3': resolution: {integrity: sha512-ECAqfpNsef+7MO8qtR0h9KcFIBAygaE7Cm6UOiQl+ft+uVap+1G7bNEjs4mdJE2OnA4m6k7i8peH8uGIAsOMGw==} engines: {node: '>=20.0.0'} + '@aws-sdk/types@3.974.5': + resolution: {integrity: sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ==} + engines: {node: '>=20.0.0'} + '@aws-sdk/xml-builder@3.972.38': resolution: {integrity: sha512-grf7mzfVxBS5AlsuTvBN7uDpzqohFww9fRPCO+EBSUdvtsYMcPSKdz54h/7XiscqNcUM1Ae1MF7JLHmiYYuzbQ==} engines: {node: '>=20.0.0'} + '@aws-sdk/xml-builder@3.972.40': + resolution: {integrity: sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA==} + engines: {node: '>=20.0.0'} + '@aws/lambda-invoke-store@0.3.0': resolution: {integrity: sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==} engines: {node: '>=18.0.0'} @@ -3944,6 +4010,10 @@ packages: resolution: {integrity: sha512-NAiCSC78fzbNIEWoheoF74Ob5ZorLijCHpMY26Fqvqg/+9LuyIqMfHDg2p8Yk1rqOyowtiL3y7WX0AW+teL6zw==} engines: {node: '>=18.0.0'} + '@smithy/core@3.33.3': + resolution: {integrity: sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg==} + engines: {node: '>=18.0.0'} + '@smithy/credential-provider-imds@4.5.0': resolution: {integrity: sha512-2jsPi+7Zv2hSzD9IXR9D7DTqSn7mv4XalzRm+bESh53jiaUS3NKEUbpQFTJP0HhQy9qzZvluxQ3yS24zdRrqsA==} engines: {node: '>=18.0.0'} @@ -3952,10 +4022,18 @@ packages: resolution: {integrity: sha512-W/exA8T0LEzCQtJ02w4IzaEQPIspgarqZprb7W8FwnYiDowgCrjl2fTQ6FvuSSUnJORuepBF81abmBJwqh+0XQ==} engines: {node: '>=18.0.0'} + '@smithy/fetch-http-handler@5.8.0': + resolution: {integrity: sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA==} + engines: {node: '>=18.0.0'} + '@smithy/node-http-handler@4.10.0': resolution: {integrity: sha512-nrh7VxqzPQS/ip1hS293aI/OAWDWARQvjUxCfuKhyrfHa2gTdk28066RNeWLI1uuoHXaKAkOF8IcSAHuOp0+SA==} engines: {node: '>=18.0.0'} + '@smithy/node-http-handler@4.12.1': + resolution: {integrity: sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw==} + engines: {node: '>=18.0.0'} + '@smithy/signature-v4@5.7.0': resolution: {integrity: sha512-hCynhm22wMJ8wTF9crcwu8mxggtUrSLLJgDcGUvYFBqpofxycYJCGKOMYg4xtPPFtgNiDJSYmhsWLTrcU/g59Q==} engines: {node: '>=18.0.0'} @@ -3964,6 +4042,10 @@ packages: resolution: {integrity: sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg==} engines: {node: '>=18.0.0'} + '@smithy/types@4.18.0': + resolution: {integrity: sha512-CgB6HHWer/vrKps24ulRIbpcpb7K4xAU7SkZ7YHzBPlwHsvsrCJFEXK421s+cJzX+ZrqtA/TuU5w1HzI7k9N8A==} + engines: {node: '>=18.0.0'} + '@stablelib/base64@1.0.1': resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} @@ -9315,6 +9397,17 @@ snapshots: escape-html: 1.0.3 xpath: 0.0.32 + '@aws-sdk/client-cloudwatch-logs@3.1127.0': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/credential-provider-node': 3.972.82 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/fetch-http-handler': 5.8.0 + '@smithy/node-http-handler': 4.12.1 + '@smithy/types': 4.18.0 + tslib: 2.8.1 + '@aws-sdk/client-route-53@3.1108.0': dependencies: '@aws-sdk/core': 3.977.7 @@ -9360,6 +9453,17 @@ snapshots: bowser: 2.14.1 tslib: 2.8.1 + '@aws-sdk/core@3.977.9': + dependencies: + '@aws-sdk/types': 3.974.5 + '@aws-sdk/xml-builder': 3.972.40 + '@aws/lambda-invoke-store': 0.3.0 + '@smithy/core': 3.33.3 + '@smithy/signature-v4': 5.7.0 + '@smithy/types': 4.18.0 + bowser: 2.14.1 + tslib: 2.8.1 + '@aws-sdk/credential-provider-env@3.972.68': dependencies: '@aws-sdk/core': 3.977.7 @@ -9368,6 +9472,14 @@ snapshots: '@smithy/types': 4.17.0 tslib: 2.8.1 + '@aws-sdk/credential-provider-env@3.972.70': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.18.0 + tslib: 2.8.1 + '@aws-sdk/credential-provider-http@3.972.70': dependencies: '@aws-sdk/core': 3.977.7 @@ -9378,6 +9490,16 @@ snapshots: '@smithy/types': 4.17.0 tslib: 2.8.1 + '@aws-sdk/credential-provider-http@3.972.72': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/fetch-http-handler': 5.8.0 + '@smithy/node-http-handler': 4.12.1 + '@smithy/types': 4.18.0 + tslib: 2.8.1 + '@aws-sdk/credential-provider-ini@3.973.13': dependencies: '@aws-sdk/core': 3.977.7 @@ -9394,6 +9516,22 @@ snapshots: '@smithy/types': 4.17.0 tslib: 2.8.1 + '@aws-sdk/credential-provider-ini@3.973.15': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/credential-provider-env': 3.972.70 + '@aws-sdk/credential-provider-http': 3.972.72 + '@aws-sdk/credential-provider-login': 3.972.77 + '@aws-sdk/credential-provider-process': 3.972.70 + '@aws-sdk/credential-provider-sso': 3.973.14 + '@aws-sdk/credential-provider-web-identity': 3.972.76 + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/credential-provider-imds': 4.5.0 + '@smithy/types': 4.18.0 + tslib: 2.8.1 + '@aws-sdk/credential-provider-login@3.972.75': dependencies: '@aws-sdk/core': 3.977.7 @@ -9403,6 +9541,15 @@ snapshots: '@smithy/types': 4.17.0 tslib: 2.8.1 + '@aws-sdk/credential-provider-login@3.972.77': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.18.0 + tslib: 2.8.1 + '@aws-sdk/credential-provider-node@3.972.79': dependencies: '@aws-sdk/credential-provider-env': 3.972.68 @@ -9417,6 +9564,20 @@ snapshots: '@smithy/types': 4.17.0 tslib: 2.8.1 + '@aws-sdk/credential-provider-node@3.972.82': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.70 + '@aws-sdk/credential-provider-http': 3.972.72 + '@aws-sdk/credential-provider-ini': 3.973.15 + '@aws-sdk/credential-provider-process': 3.972.70 + '@aws-sdk/credential-provider-sso': 3.973.14 + '@aws-sdk/credential-provider-web-identity': 3.972.76 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/credential-provider-imds': 4.5.0 + '@smithy/types': 4.18.0 + tslib: 2.8.1 + '@aws-sdk/credential-provider-process@3.972.68': dependencies: '@aws-sdk/core': 3.977.7 @@ -9425,6 +9586,14 @@ snapshots: '@smithy/types': 4.17.0 tslib: 2.8.1 + '@aws-sdk/credential-provider-process@3.972.70': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.18.0 + tslib: 2.8.1 + '@aws-sdk/credential-provider-sso@3.973.12': dependencies: '@aws-sdk/core': 3.977.7 @@ -9435,6 +9604,16 @@ snapshots: '@smithy/types': 4.17.0 tslib: 2.8.1 + '@aws-sdk/credential-provider-sso@3.973.14': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/token-providers': 3.1116.0 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.18.0 + tslib: 2.8.1 + '@aws-sdk/credential-provider-web-identity@3.972.74': dependencies: '@aws-sdk/core': 3.977.7 @@ -9444,6 +9623,15 @@ snapshots: '@smithy/types': 4.17.0 tslib: 2.8.1 + '@aws-sdk/credential-provider-web-identity@3.972.76': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.18.0 + tslib: 2.8.1 + '@aws-sdk/middleware-sdk-route53@3.972.24': dependencies: '@aws-sdk/types': 3.974.3 @@ -9461,6 +9649,17 @@ snapshots: '@smithy/types': 4.17.0 tslib: 2.8.1 + '@aws-sdk/nested-clients@3.997.44': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/signature-v4-multi-region': 3.996.46 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/fetch-http-handler': 5.8.0 + '@smithy/node-http-handler': 4.12.1 + '@smithy/types': 4.18.0 + tslib: 2.8.1 + '@aws-sdk/signature-v4-multi-region@3.996.44': dependencies: '@aws-sdk/types': 3.974.3 @@ -9468,6 +9667,13 @@ snapshots: '@smithy/types': 4.17.0 tslib: 2.8.1 + '@aws-sdk/signature-v4-multi-region@3.996.46': + dependencies: + '@aws-sdk/types': 3.974.5 + '@smithy/signature-v4': 5.7.0 + '@smithy/types': 4.18.0 + tslib: 2.8.1 + '@aws-sdk/token-providers@3.1108.0': dependencies: '@aws-sdk/core': 3.977.7 @@ -9477,16 +9683,35 @@ snapshots: '@smithy/types': 4.17.0 tslib: 2.8.1 + '@aws-sdk/token-providers@3.1116.0': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/nested-clients': 3.997.44 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/types': 4.18.0 + tslib: 2.8.1 + '@aws-sdk/types@3.974.3': dependencies: '@smithy/types': 4.17.0 tslib: 2.8.1 + '@aws-sdk/types@3.974.5': + dependencies: + '@smithy/types': 4.18.0 + tslib: 2.8.1 + '@aws-sdk/xml-builder@3.972.38': dependencies: '@smithy/types': 4.17.0 tslib: 2.8.1 + '@aws-sdk/xml-builder@3.972.40': + dependencies: + '@smithy/types': 4.18.0 + tslib: 2.8.1 + '@aws/lambda-invoke-store@0.3.0': {} '@babel/code-frame@7.29.0': @@ -12820,6 +13045,11 @@ snapshots: '@smithy/types': 4.17.0 tslib: 2.8.1 + '@smithy/core@3.33.3': + dependencies: + '@smithy/types': 4.18.0 + tslib: 2.8.1 + '@smithy/credential-provider-imds@4.5.0': dependencies: '@smithy/core': 3.32.0 @@ -12832,12 +13062,24 @@ snapshots: '@smithy/types': 4.17.0 tslib: 2.8.1 + '@smithy/fetch-http-handler@5.8.0': + dependencies: + '@smithy/core': 3.33.3 + '@smithy/types': 4.18.0 + tslib: 2.8.1 + '@smithy/node-http-handler@4.10.0': dependencies: '@smithy/core': 3.32.0 '@smithy/types': 4.17.0 tslib: 2.8.1 + '@smithy/node-http-handler@4.12.1': + dependencies: + '@smithy/core': 3.33.3 + '@smithy/types': 4.18.0 + tslib: 2.8.1 + '@smithy/signature-v4@5.7.0': dependencies: '@smithy/core': 3.32.0 @@ -12848,6 +13090,10 @@ snapshots: dependencies: tslib: 2.8.1 + '@smithy/types@4.18.0': + dependencies: + tslib: 2.8.1 + '@stablelib/base64@1.0.1': {} '@standard-schema/spec@1.1.0': {}