diff --git a/README.md b/README.md index 6a72f10d9..4021b6c33 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. +- **Logs & Metrics**: Ship container logs and host/container metrics to Grafana (Loki, Prometheus/Mimir), Datadog, New Relic, InfluxDB, VictoriaMetrics, Better Stack, Elasticsearch/OpenSearch, Splunk, or AWS CloudWatch, 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__/compose/domain/raw-compose.test.ts b/apps/dokploy/__test__/compose/domain/raw-compose.test.ts index abee0dc59..4c6373d28 100644 --- a/apps/dokploy/__test__/compose/domain/raw-compose.test.ts +++ b/apps/dokploy/__test__/compose/domain/raw-compose.test.ts @@ -33,8 +33,16 @@ services: volumes: test-data: `, + composeId: "compose-1", + name: "raw-stack", composePath: "./docker-compose.yml", composeType: "stack", + environment: { + environmentId: "env-1", + name: "production", + projectId: "project-1", + project: { name: "shop", organizationId: "org-1" }, + }, isolatedDeployment: false, isolatedDeploymentsVolume: false, randomize: false, @@ -54,6 +62,72 @@ volumes: }, ]); expect(converted?.services?.test?.tmpfs).toEqual(["/cache"]); + expect(converted?.services?.test?.labels).toEqual( + expect.arrayContaining([ + "dokploy.organization.id=org-1", + "dokploy.project=shop", + "dokploy.service=test", + ]), + ); + expect(converted?.services?.test?.deploy?.labels).toEqual( + expect.arrayContaining(["dokploy.organization.id=org-1"]), + ); expect(execAsyncRemote).not.toHaveBeenCalled(); }); + + it("escapes `$` in the Dokploy label values so compose keeps them literal", async () => { + const compose = { + appName: "dollar-stack", + composeFile: ` +services: + mapped: + image: alpine:latest + labels: + custom: keep + deploy: + labels: + custom: keep + listed: + image: alpine:latest + labels: + - custom=keep +`, + composeId: "compose-1", + name: "app $DB_PASSWORD", + composePath: "./docker-compose.yml", + composeType: "stack", + environment: { + environmentId: "env-1", + name: "env ${HOME}", + projectId: "project-1", + project: { name: "Budget $100", organizationId: "org-1" }, + }, + isolatedDeployment: false, + isolatedDeploymentsVolume: false, + randomize: false, + serverId: null, + sourceType: "raw", + suffix: "", + } as unknown as Parameters[0]; + + const converted = await addDomainToCompose(compose, []); + const escaped = { + "dokploy.project": "Budget $$100", + "dokploy.environment": "env $${HOME}", + "dokploy.application": "app $$DB_PASSWORD", + }; + + const mapped = converted?.services?.mapped; + expect(mapped?.labels).toMatchObject({ custom: "keep", ...escaped }); + expect(mapped?.deploy?.labels).toMatchObject({ + custom: "keep", + ...escaped, + }); + const listed = converted?.services?.listed; + const asList = Object.entries(escaped).map(([k, v]) => `${k}=${v}`); + expect(listed?.labels).toEqual( + expect.arrayContaining(["custom=keep", ...asList]), + ); + expect(listed?.deploy?.labels).toEqual(expect.arrayContaining(asList)); + }); }); diff --git a/apps/dokploy/__test__/dns/ovh.test.ts b/apps/dokploy/__test__/dns/ovh.test.ts index 7e08d7f99..f41735682 100644 --- a/apps/dokploy/__test__/dns/ovh.test.ts +++ b/apps/dokploy/__test__/dns/ovh.test.ts @@ -166,6 +166,16 @@ describe("ovhClient.listZones", () => { await expect(ovhClient.listZones(cfg)).rejects.toThrow("Invalid signature"); }); + + it("returns only the configured zone without listing every zone", async () => { + const cfg = { ...freshConfig(), zone: "example.com" }; + mockApi(); + + const zones = await ovhClient.listZones(cfg); + + expect(zones).toEqual([{ id: "example.com", name: "example.com" }]); + expect(apiCalls()).toHaveLength(0); + }); }); describe("ovhClient.listRecords", () => { @@ -578,4 +588,55 @@ describe("ovhClient.testConnection", () => { "Invalid signature", ); }); + + it("checks the configured zone instead of listing all zones", async () => { + const cfg = { ...freshConfig(), zone: "example.com" }; + mockApi(ovhSuccess({ name: "example.com" })); + + await expect(ovhClient.testConnection(cfg)).resolves.toBeUndefined(); + + const calls = apiCalls(); + expect(calls).toHaveLength(1); + expect(calls[0]?.[0]).toBe(`${cfg.baseUrl}/domain/zone/example.com`); + }); +}); + +describe("ovhClient with a configured zone", () => { + const record = { + type: "A" as const, + name: "app.other.org", + content: "1.2.3.4", + }; + + it("refuses record operations on any other zone without calling OVH", async () => { + const cfg = { ...freshConfig(), zone: "example.com" }; + mockApi(); + + const message = /restricted to the zone "example.com"/; + await expect(ovhClient.listRecords(cfg, "other.org")).rejects.toThrow( + message, + ); + await expect( + ovhClient.upsertRecord(cfg, { ...record, zoneId: "other.org" }), + ).rejects.toThrow(message); + await expect( + ovhClient.updateRecord(cfg, "other.org", "1", record), + ).rejects.toThrow(message); + await expect(ovhClient.deleteRecord(cfg, "other.org", "1")).rejects.toThrow( + message, + ); + expect(apiCalls()).toHaveLength(0); + }); + + it("still allows operations on the configured zone", async () => { + const cfg = { ...freshConfig(), zone: "example.com" }; + mockApi(ovhSuccess(null), ovhSuccess(null)); + + await expect( + ovhClient.deleteRecord(cfg, "example.com", "1"), + ).resolves.toBeUndefined(); + expect(apiCalls()[0]?.[0]).toBe( + `${cfg.baseUrl}/domain/zone/example.com/record/1`, + ); + }); }); diff --git a/apps/dokploy/__test__/git-provider/gitlab-refresh-token.test.ts b/apps/dokploy/__test__/git-provider/gitlab-refresh-token.test.ts new file mode 100644 index 000000000..71b4421f3 --- /dev/null +++ b/apps/dokploy/__test__/git-provider/gitlab-refresh-token.test.ts @@ -0,0 +1,99 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const store = vi.hoisted(() => ({ + provider: {} as Record, +})); + +vi.mock("@dokploy/server/services/gitlab", () => ({ + findGitlabById: vi.fn(async () => ({ ...store.provider })), + updateGitlab: vi.fn(async (_id: string, values: Record) => { + store.provider = { ...store.provider, ...values }; + }), +})); + +const { refreshGitlabToken } = await import( + "@dokploy/server/utils/providers/gitlab" +); + +const GITLAB_ID = "gitlab-1"; + +// Behaves like GitLab: a refresh returns a new pair and invalidates the refresh token it used +const createGitlab = () => { + let validRefreshToken = "refresh-1"; + let issued = 1; + let failNext = false; + + const fetch = vi.fn(async (_url: string, init: RequestInit) => { + const body = new URLSearchParams(init.body as string); + // Keeps the request open, so concurrent callers overlap as two deployments do + await new Promise((resolve) => setTimeout(resolve, 20)); + + if (failNext) { + failNext = false; + return new Response(null, { + status: 500, + statusText: "Internal Server Error", + }); + } + + if (body.get("refresh_token") !== validRefreshToken) { + return new Response(null, { status: 400, statusText: "Bad Request" }); + } + + issued++; + validRefreshToken = `refresh-${issued}`; + return Response.json({ + access_token: `access-${issued}`, + refresh_token: validRefreshToken, + expires_in: 7200, + }); + }); + + return { + fetch, + failNextRequest: () => { + failNext = true; + }, + }; +}; + +describe("refreshGitlabToken", () => { + let gitlab: ReturnType; + + beforeEach(() => { + store.provider = { + gitlabId: GITLAB_ID, + gitlabUrl: "https://gitlab.example.com", + gitlabInternalUrl: null, + applicationId: "app-id", + secret: "app-secret", + accessToken: "access-1", + refreshToken: "refresh-1", + expiresAt: 0, + }; + gitlab = createGitlab(); + vi.stubGlobal("fetch", gitlab.fetch); + }); + + it("sends one refresh request when two callers find the token expired at the same time", async () => { + await Promise.all([ + refreshGitlabToken(GITLAB_ID), + refreshGitlabToken(GITLAB_ID), + ]); + + expect(gitlab.fetch).toHaveBeenCalledTimes(1); + expect(store.provider.refreshToken).toBe("refresh-2"); + }); + + it("refreshes again after a failed refresh", async () => { + gitlab.failNextRequest(); + + await expect(refreshGitlabToken(GITLAB_ID)).rejects.toThrow( + "Failed to refresh token: Internal Server Error", + ); + await refreshGitlabToken(GITLAB_ID); + + expect(gitlab.fetch).toHaveBeenCalledTimes(2); + expect(store.provider.refreshToken).toBe("refresh-2"); + }); +}); diff --git a/apps/dokploy/__test__/logs-and-metrics/account-deletion-vector.test.ts b/apps/dokploy/__test__/logs-and-metrics/account-deletion-vector.test.ts new file mode 100644 index 000000000..2feaa3438 --- /dev/null +++ b/apps/dokploy/__test__/logs-and-metrics/account-deletion-vector.test.ts @@ -0,0 +1,56 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + findOwnedOrganizations: vi.fn(), + transaction: vi.fn(), + removeVectorAgents: vi.fn(), + calls: [] as string[], +})); + +vi.mock("@dokploy/server/db", () => ({ + db: { + query: { organization: { findMany: mocks.findOwnedOrganizations } }, + transaction: mocks.transaction, + }, +})); +vi.mock("@dokploy/server/services/organization", () => ({ + removeVectorAgents: mocks.removeVectorAgents, +})); + +const { deleteUserAccountData } = await import( + "@dokploy/server/services/account-deletion" +); + +beforeEach(() => { + mocks.calls.length = 0; + mocks.findOwnedOrganizations.mockReset(); + mocks.removeVectorAgents + .mockReset() + .mockImplementation(async (id: string) => mocks.calls.push(`agents:${id}`)); + mocks.transaction.mockReset().mockImplementation(async () => { + mocks.calls.push("delete"); + return { email: "a@b.c", organizationsDeleted: 2 }; + }); +}); + +describe("deleteUserAccountData", () => { + it("removes the Vector agents of every owned organization before deleting the user", async () => { + mocks.findOwnedOrganizations.mockResolvedValue([ + { id: "org-a" }, + { id: "org-b" }, + ]); + + await deleteUserAccountData("user-1"); + + expect(mocks.calls).toEqual(["agents:org-a", "agents:org-b", "delete"]); + }); + + it("deletes the user without touching agents when it owns no organization", async () => { + mocks.findOwnedOrganizations.mockResolvedValue([]); + + await deleteUserAccountData("user-1"); + + expect(mocks.removeVectorAgents).not.toHaveBeenCalled(); + expect(mocks.calls).toEqual(["delete"]); + }); +}); diff --git a/apps/dokploy/__test__/logs-and-metrics/metrics-adapters.test.ts b/apps/dokploy/__test__/logs-and-metrics/metrics-adapters.test.ts new file mode 100644 index 000000000..76ba498ce --- /dev/null +++ b/apps/dokploy/__test__/logs-and-metrics/metrics-adapters.test.ts @@ -0,0 +1,542 @@ +import { + getTelemetryProviderAdapter, + telemetryProviderAdapters, +} from "@dokploy/server/services/logs-and-metrics/providers/registry"; +import type { TelemetryProviderRuntimeConfig } from "@dokploy/server/services/logs-and-metrics/types"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const aws = vi.hoisted(() => ({ + describeLogGroups: vi.fn(), + putMetricData: vi.fn(), +})); + +vi.mock("node:dns/promises", () => ({ + lookup: async () => [{ address: "93.184.216.34" }], +})); +vi.mock("@aws-sdk/client-cloudwatch-logs", () => ({ + CloudWatchLogsClient: class { + send = aws.describeLogGroups; + }, + DescribeLogGroupsCommand: class { + constructor(public input: unknown) {} + }, +})); +vi.mock("@aws-sdk/client-cloudwatch", () => ({ + CloudWatchClient: class { + send = aws.putMetricData; + }, + PutMetricDataCommand: class { + constructor(public input: unknown) {} + }, +})); + +const baseConfig: TelemetryProviderRuntimeConfig = { + telemetryProviderId: "provider-1", + name: "test", + signals: ["metrics"], + endpoint: null, + apiKey: null, + apiSecret: null, + extraConfig: null, +}; + +const originalFetch = global.fetch; +const fetchMock = vi.fn(); + +const respond = (status: number, body: unknown = "") => + new Response( + status === 204 + ? null + : typeof body === "string" + ? body + : JSON.stringify(body), + { status }, + ); + +beforeEach(() => { + fetchMock.mockReset(); + aws.describeLogGroups.mockReset(); + aws.putMetricData.mockReset(); + global.fetch = fetchMock as unknown as typeof fetch; +}); + +afterEach(() => { + global.fetch = originalFetch; +}); + +const calledUrls = () => fetchMock.mock.calls.map((call) => String(call[0])); + +describe("registry", () => { + it("resolves the nine provider types, each with a non-empty valid signal list", () => { + const types = [ + "loki", + "prometheus_remote_write", + "new_relic", + "influxdb", + "datadog", + "aws_cloudwatch", + "splunk_hec", + "elasticsearch", + "betterstack", + ] as const; + expect(Object.keys(telemetryProviderAdapters).sort()).toEqual( + [...types].sort(), + ); + for (const type of types) { + const adapter = getTelemetryProviderAdapter(type); + expect(adapter.type).toBe(type); + expect(adapter.signals.length).toBeGreaterThan(0); + expect( + adapter.signals.every((s) => s === "logs" || s === "metrics"), + ).toBe(true); + } + expect(telemetryProviderAdapters.loki.signals).toEqual(["logs"]); + expect(telemetryProviderAdapters.prometheus_remote_write.signals).toEqual([ + "metrics", + ]); + expect(telemetryProviderAdapters.datadog.signals).toEqual([ + "logs", + "metrics", + ]); + }); +}); + +describe("prometheusRemoteWriteAdapter", () => { + const adapter = telemetryProviderAdapters.prometheus_remote_write; + const endpoint = "http://prom.example.com:9090/api/v1/write"; + + it("builds a sink without auth, with the healthcheck disabled", () => { + const sink = adapter.toVectorSink( + { ...baseConfig, endpoint }, + "sink_1", + "dokploy_metrics_scope", + "metrics", + ); + expect(sink).toMatchObject({ + type: "prometheus_remote_write", + inputs: ["dokploy_metrics_scope"], + endpoint, + healthcheck: { enabled: false }, + }); + expect(sink.auth).toBeUndefined(); + expect(sink.tenant_id).toBeUndefined(); + }); + + it("uses basic auth with a username, bearer with a token alone, and sets tenant_id", () => { + const basic = adapter.toVectorSink( + { + ...baseConfig, + endpoint, + apiKey: "pw", + extraConfig: { username: "12345", tenantId: "team-a" }, + }, + "sink_1", + "scope", + "metrics", + ); + expect(basic.auth).toEqual({ + strategy: "basic", + user: "12345", + password: "pw", + }); + expect(basic.tenant_id).toBe("team-a"); + + const bearer = adapter.toVectorSink( + { ...baseConfig, endpoint, apiKey: "tok" }, + "sink_1", + "scope", + "metrics", + ); + expect(bearer.auth).toEqual({ strategy: "bearer", token: "tok" }); + }); + + it("rejects an endpoint that is not an http(s) URL", () => { + expect(() => + adapter.validateConfig?.({ ...baseConfig, endpoint: "not a url" }), + ).toThrow(/valid http\(s\) URL/); + expect(() => + adapter.validateConfig?.({ ...baseConfig, endpoint: "ftp://x" }), + ).toThrow(/valid http\(s\) URL/); + }); + + it("treats 2xx and 400 as success and gives distinct errors for 401 and 404", async () => { + fetchMock.mockResolvedValueOnce(respond(204)); + await expect( + adapter.testConnection?.({ ...baseConfig, endpoint }), + ).resolves.toBeUndefined(); + + fetchMock.mockResolvedValueOnce(respond(400)); + await expect( + adapter.testConnection?.({ ...baseConfig, endpoint }), + ).resolves.toBeUndefined(); + + fetchMock.mockResolvedValueOnce(respond(401)); + await expect( + adapter.testConnection?.({ ...baseConfig, endpoint, apiKey: "bad" }), + ).rejects.toThrow(/rejected the credentials \(status 401\)/); + + fetchMock.mockResolvedValueOnce(respond(404)); + await expect( + adapter.testConnection?.({ ...baseConfig, endpoint }), + ).rejects.toThrow(/not found at this URL \(status 404\)/); + }); + + it("sends the auth header and the tenant header on the test request", async () => { + fetchMock.mockResolvedValueOnce(respond(204)); + await adapter.testConnection?.({ + ...baseConfig, + endpoint, + apiKey: "tok", + extraConfig: { tenantId: "team-a" }, + }); + const init = fetchMock.mock.calls[0]?.[1] as RequestInit; + expect(init.method).toBe("POST"); + expect(init.headers).toMatchObject({ + Authorization: "Bearer tok", + "X-Scope-OrgID": "team-a", + }); + }); +}); + +describe("newRelicAdapter", () => { + const adapter = telemetryProviderAdapters.new_relic; + const config = { + ...baseConfig, + apiKey: "license", + extraConfig: { accountId: "123" }, + }; + + it("defaults to the us region and accepts eu", () => { + expect(adapter.toVectorSink(config, "s", "scope", "metrics")).toMatchObject( + { + type: "new_relic", + api: "metrics", + license_key: "license", + account_id: "123", + region: "us", + }, + ); + expect( + adapter.toVectorSink( + { ...config, extraConfig: { accountId: "123", region: "EU" } }, + "s", + "scope", + "metrics", + ).region, + ).toBe("eu"); + }); + + it("rejects an unknown region", () => { + expect(() => + adapter.validateConfig?.({ + ...config, + extraConfig: { accountId: "123", region: "jp" }, + }), + ).toThrow(/"us" or "eu"/); + }); + + it("posts a gauge to the regional metric API: 202 passes, 403 is an invalid key", async () => { + fetchMock.mockResolvedValueOnce(respond(202)); + await expect( + adapter.testConnection?.({ + ...config, + extraConfig: { accountId: "123", region: "eu" }, + }), + ).resolves.toBeUndefined(); + expect(calledUrls()[0]).toBe( + "https://metric-api.eu.newrelic.com/metric/v1", + ); + + fetchMock.mockResolvedValueOnce(respond(403)); + await expect(adapter.testConnection?.(config)).rejects.toThrow( + /rejected the license key/, + ); + expect(calledUrls()[1]).toBe("https://metric-api.newrelic.com/metric/v1"); + }); +}); + +describe("influxdbAdapter", () => { + const adapter = telemetryProviderAdapters.influxdb; + const config = { + ...baseConfig, + endpoint: "http://influx.example.com:8086", + apiKey: "token", + extraConfig: { org: "dokploy", bucket: "metrics" }, + }; + + it("builds an influxdb_metrics v2 sink", () => { + expect(adapter.toVectorSink(config, "s", "scope", "metrics")).toEqual({ + type: "influxdb_metrics", + inputs: ["scope"], + endpoint: "http://influx.example.com:8086", + org: "dokploy", + bucket: "metrics", + token: "token", + default_namespace: "dokploy", + version: "2", + buffer: { type: "disk", max_size: 268_435_488, when_full: "block" }, + }); + }); + + it("writes a test point: 204 passes, 401 and 404 give distinct errors", async () => { + fetchMock.mockResolvedValueOnce(respond(204)); + await expect(adapter.testConnection?.(config)).resolves.toBeUndefined(); + expect(calledUrls()[0]).toBe( + "http://influx.example.com:8086/api/v2/write?org=dokploy&bucket=metrics&precision=s", + ); + + fetchMock.mockResolvedValueOnce(respond(401)); + await expect(adapter.testConnection?.(config)).rejects.toThrow( + /rejected the token/, + ); + + fetchMock.mockResolvedValueOnce(respond(404)); + await expect(adapter.testConnection?.(config)).rejects.toThrow( + /organization or bucket not found/, + ); + }); +}); + +describe("datadogAdapter with metrics", () => { + const adapter = telemetryProviderAdapters.datadog; + const config = { + ...baseConfig, + signals: ["logs", "metrics"] as const, + apiKey: "dd", + }; + + it("ships metrics through datadog_metrics without a transform", () => { + expect( + adapter.toVectorTransform?.(config as never, "t", "scope", "metrics"), + ).toBeNull(); + expect( + adapter.toVectorSink(config as never, "s", "scope", "metrics"), + ).toMatchObject({ + type: "datadog_metrics", + inputs: ["scope"], + default_api_key: "dd", + site: "datadoghq.com", + }); + }); + + it("keeps the logs sink and ddtags transform unchanged", () => { + expect( + adapter.toVectorTransform?.(config as never, "t", "scope", "logs"), + ).toMatchObject({ type: "remap", inputs: ["scope"] }); + expect(adapter.toVectorSink(config as never, "s", "t", "logs").type).toBe( + "datadog_logs", + ); + }); +}); + +describe("betterStackAdapter with metrics", () => { + const adapter = telemetryProviderAdapters.betterstack; + const config = { + ...baseConfig, + endpoint: "in.logs.betterstack.com", + apiKey: "token", + }; + + it("posts metrics to /metrics without a transform", () => { + expect( + adapter.toVectorTransform?.(config, "t", "scope", "metrics"), + ).toBeNull(); + expect(adapter.toVectorSink(config, "s", "scope", "metrics")).toMatchObject( + { + type: "http", + uri: "https://in.logs.betterstack.com/metrics", + auth: { strategy: "bearer", token: "token" }, + }, + ); + expect(adapter.toVectorSink(config, "s", "scope", "logs").uri).toBe( + "https://in.logs.betterstack.com/", + ); + }); + + it("tests each selected signal: two requests for logs and metrics, one for logs only", async () => { + fetchMock.mockResolvedValue(respond(202)); + await adapter.testConnection?.({ ...config, signals: ["logs", "metrics"] }); + expect(calledUrls()).toEqual([ + "https://in.logs.betterstack.com/", + "https://in.logs.betterstack.com/metrics", + ]); + + fetchMock.mockClear(); + await adapter.testConnection?.({ ...config, signals: ["logs"] }); + expect(calledUrls()).toEqual(["https://in.logs.betterstack.com/"]); + }); +}); + +describe("awsCloudwatchAdapter with metrics", () => { + const adapter = telemetryProviderAdapters.aws_cloudwatch; + const config = { + ...baseConfig, + apiKey: "AKIA", + apiSecret: "secret", + extraConfig: { + region: "us-east-1", + logGroup: "/dokploy/logs", + metricsNamespace: "Dokploy", + }, + }; + + it("scopes the log fields to logs and the namespace to metrics", () => { + const bySignal = Object.fromEntries( + adapter.credentialFields.map((f) => [f.key, f.signal]), + ); + expect(bySignal).toMatchObject({ + logGroup: "logs", + logStream: "logs", + metricsNamespace: "metrics", + }); + expect(bySignal.region).toBeUndefined(); + }); + + it("rejects a namespace starting with AWS/ only when metrics are selected", () => { + const bad = { + ...config, + extraConfig: { ...config.extraConfig, metricsNamespace: "AWS/Foo" }, + }; + expect(() => adapter.validateConfig?.(bad)).toThrow( + /cannot start with AWS\//, + ); + expect(() => + adapter.validateConfig?.({ ...bad, signals: ["logs"] }), + ).not.toThrow(); + }); + + it("filters metrics to the allowlist and sets the namespace in the transform", () => { + const transform = adapter.toVectorTransform?.( + config, + "t", + "scope", + "metrics", + ); + expect(transform).toMatchObject({ + type: "remap", + inputs: ["scope"], + drop_on_abort: true, + }); + expect(transform?.source).toContain( + 'if !includes(["cpu_seconds_total","memory_used_bytes","memory_total_bytes","filesystem_used_bytes","filesystem_total_bytes","network_receive_bytes_total","network_transmit_bytes_total","load1","load5","load15","container_cpu_usage_seconds_total","container_memory_usage_bytes","container_network_receive_bytes_total","container_network_transmit_bytes_total"], .name) { abort }', + ); + expect(transform?.source).toContain('.namespace = "Dokploy"'); + expect(adapter.toVectorSink(config, "s", "t", "metrics")).toMatchObject({ + type: "aws_cloudwatch_metrics", + inputs: ["t"], + default_namespace: "Dokploy", + region: "us-east-1", + auth: { access_key_id: "AKIA", secret_access_key: "secret" }, + }); + }); + + it("tests metrics with PutMetricData only, and translates AccessDenied", async () => { + aws.putMetricData.mockResolvedValue({}); + await adapter.testConnection?.({ ...config, signals: ["metrics"] }); + expect(aws.putMetricData).toHaveBeenCalledTimes(1); + expect(aws.describeLogGroups).not.toHaveBeenCalled(); + expect(aws.putMetricData.mock.calls[0]?.[0]).toMatchObject({ + input: { Namespace: "Dokploy" }, + }); + + aws.putMetricData.mockRejectedValue( + Object.assign(new Error("denied"), { name: "AccessDenied" }), + ); + await expect( + adapter.testConnection?.({ ...config, signals: ["metrics"] }), + ).rejects.toThrow(/needs cloudwatch:PutMetricData/); + }); + + it("tests logs with DescribeLogGroups only", async () => { + aws.describeLogGroups.mockResolvedValue({}); + await adapter.testConnection?.({ ...config, signals: ["logs"] }); + expect(aws.describeLogGroups).toHaveBeenCalledTimes(1); + expect(aws.putMetricData).not.toHaveBeenCalled(); + }); +}); + +describe("splunkAdapter with metrics", () => { + const adapter = telemetryProviderAdapters.splunk_hec; + const config = { + ...baseConfig, + endpoint: "https://splunk.example.com:8088", + apiKey: "hec", + extraConfig: { metricsIndex: "dokploy_metrics" }, + }; + + it("requires the metrics index only for metrics", () => { + const field = adapter.credentialFields.find( + (f) => f.key === "metricsIndex", + ); + expect(field).toMatchObject({ required: true, signal: "metrics" }); + expect( + adapter.credentialFields.find((f) => f.key === "index")?.signal, + ).toBe("logs"); + }); + + it("builds a splunk_hec_metrics sink", () => { + expect(adapter.toVectorSink(config, "s", "scope", "metrics")).toMatchObject( + { + type: "splunk_hec_metrics", + endpoint: "https://splunk.example.com:8088", + default_token: "hec", + index: "dokploy_metrics", + source: "dokploy", + sourcetype: "dokploy:metrics", + }, + ); + }); + + it("writes a test metric and translates HEC code 7", async () => { + fetchMock.mockResolvedValueOnce(respond(200, { text: "Success", code: 0 })); + await expect(adapter.testConnection?.(config)).resolves.toBeUndefined(); + expect(calledUrls()).toEqual([ + "https://splunk.example.com:8088/services/collector", + ]); + + fetchMock.mockResolvedValueOnce( + respond(400, { text: "Incorrect index", code: 7 }), + ); + await expect(adapter.testConnection?.(config)).rejects.toThrow( + /can't write to the metrics index "dokploy_metrics"/, + ); + }); + + it("only checks the health endpoint for logs", async () => { + fetchMock.mockResolvedValueOnce(respond(200)); + await adapter.testConnection?.({ ...config, signals: ["logs"] }); + expect(calledUrls()).toEqual([ + "https://splunk.example.com:8088/services/collector/health", + ]); + }); +}); + +describe("elasticsearchAdapter with metrics", () => { + const adapter = telemetryProviderAdapters.elasticsearch; + const config = { ...baseConfig, endpoint: "https://es.example.com:9200" }; + + it("uses the daily default metrics index when none is set, and the custom one otherwise", () => { + expect( + adapter.toVectorTransform?.(config, "t", "scope", "metrics"), + ).toBeNull(); + expect(adapter.toVectorSink(config, "s", "scope", "metrics")).toMatchObject( + { + type: "elasticsearch", + api_version: "v8", + bulk: { index: "dokploy-metrics-%Y.%m.%d" }, + }, + ); + expect( + adapter.toVectorSink( + { ...config, extraConfig: { metricsIndex: "metrics-custom" } }, + "s", + "scope", + "metrics", + ).bulk, + ).toEqual({ index: "metrics-custom" }); + }); + + it("keeps the logs sink without a bulk index unless one is set", () => { + expect(adapter.toVectorSink(config, "s", "t", "logs").bulk).toBeUndefined(); + }); +}); diff --git a/apps/dokploy/__test__/logs-and-metrics/organization-delete.test.ts b/apps/dokploy/__test__/logs-and-metrics/organization-delete.test.ts new file mode 100644 index 000000000..78bb638be --- /dev/null +++ b/apps/dokploy/__test__/logs-and-metrics/organization-delete.test.ts @@ -0,0 +1,85 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + findServersByOrganizationForVectorAgent: vi.fn(), + getWebServerSettings: vi.fn(), + releaseWebServerAgent: vi.fn(), + removeVectorAgent: vi.fn(), + deleteWhere: vi.fn(), +})); + +vi.mock("@dokploy/server/constants", () => ({ IS_CLOUD: false })); +vi.mock("@dokploy/server/db", () => ({ + db: { delete: () => ({ where: mocks.deleteWhere }) }, +})); +vi.mock("@dokploy/server/services/server", () => ({ + findServersByOrganizationForVectorAgent: + mocks.findServersByOrganizationForVectorAgent, +})); +vi.mock("@dokploy/server/services/web-server-settings", () => ({ + getWebServerSettings: mocks.getWebServerSettings, + releaseWebServerAgent: mocks.releaseWebServerAgent, +})); +vi.mock("@dokploy/server/setup/vector-setup", () => ({ + removeVectorAgent: mocks.removeVectorAgent, + withVectorTargetLock: (_: string | undefined, fn: () => Promise) => + fn(), +})); + +const { deleteOrganization } = await import( + "@dokploy/server/services/organization" +); + +beforeEach(() => { + for (const mock of Object.values(mocks)) mock.mockReset(); + mocks.findServersByOrganizationForVectorAgent.mockResolvedValue([]); + mocks.deleteWhere.mockResolvedValue({ count: 1 }); + vi.spyOn(console, "error").mockImplementation(() => {}); +}); + +describe("deleteOrganization", () => { + it("attempts cleanup on every server, even with an empty selection, and swallows errors", async () => { + mocks.findServersByOrganizationForVectorAgent.mockResolvedValue([ + { serverId: "server-1", telemetryProviderIds: ["lp-1"] }, + { serverId: "server-2", telemetryProviderIds: ["lp-1"] }, + { serverId: "server-3", telemetryProviderIds: [] }, + ]); + mocks.removeVectorAgent.mockImplementation(async (serverId?: string) => { + if (serverId === "server-1") throw new Error("ssh down"); + }); + mocks.getWebServerSettings.mockResolvedValue({ + vectorAgentOrganizationId: null, + }); + + await deleteOrganization("org-a"); + + expect(mocks.removeVectorAgent).toHaveBeenCalledWith("server-1"); + expect(mocks.removeVectorAgent).toHaveBeenCalledWith("server-2"); + expect(mocks.removeVectorAgent).toHaveBeenCalledWith("server-3"); + expect(mocks.deleteWhere).toHaveBeenCalled(); + }); + + it("leaves the local agent alone when another organization owns it", async () => { + mocks.getWebServerSettings.mockResolvedValue({ + vectorAgentOrganizationId: "org-b", + }); + + await deleteOrganization("org-a"); + + expect(mocks.removeVectorAgent).not.toHaveBeenCalled(); + expect(mocks.releaseWebServerAgent).not.toHaveBeenCalled(); + }); + + it("releases the local agent even when removing it fails", async () => { + mocks.getWebServerSettings.mockResolvedValue({ + vectorAgentOrganizationId: "org-a", + }); + mocks.removeVectorAgent.mockRejectedValue(new Error("docker down")); + + await deleteOrganization("org-a"); + + expect(mocks.removeVectorAgent).toHaveBeenCalledWith(); + expect(mocks.releaseWebServerAgent).toHaveBeenCalledWith("org-a"); + expect(mocks.deleteWhere).toHaveBeenCalled(); + }); +}); diff --git a/apps/dokploy/__test__/logs-and-metrics/provider-fetch.test.ts b/apps/dokploy/__test__/logs-and-metrics/provider-fetch.test.ts new file mode 100644 index 000000000..a571ca7f4 --- /dev/null +++ b/apps/dokploy/__test__/logs-and-metrics/provider-fetch.test.ts @@ -0,0 +1,220 @@ +import { createServer, type Server } from "node:http"; +import type { AddressInfo } from "node:net"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + lookup: vi.fn(), +})); + +vi.mock("node:dns", () => ({ + lookup: mocks.lookup, +})); + +const loadModule = async (isCloud: boolean) => { + vi.resetModules(); + vi.doMock("@dokploy/server/constants", () => ({ IS_CLOUD: isCloud })); + return await import( + "@dokploy/server/services/logs-and-metrics/provider-fetch" + ); +}; + +const loadFetch = async (isCloud: boolean) => + (await loadModule(isCloud)).providerFetch; + +const resolvesTo = (...addresses: string[]) => + mocks.lookup.mockImplementation( + (_host: string, _options: unknown, callback: Function) => + callback( + null, + addresses.map((address) => ({ + address, + family: address.includes(":") ? 6 : 4, + })), + ), + ); + +describe("providerFetch — blocked 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 () => { + const providerFetch = await loadFetch(false); + await expect( + providerFetch("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 metadata IP written as an IPv4-mapped IPv6 address", async () => { + const providerFetch = await loadFetch(false); + for (const address of ["[::ffff:169.254.169.254]", "[::ffff:a9fe:a9fe]"]) { + await expect( + providerFetch(`http://${address}/latest/meta-data/`), + `expected ${address} to be rejected`, + ).rejects.toThrow(/metadata/i); + } + expect(global.fetch).not.toHaveBeenCalled(); + }); + + it("rejects the AWS ECS metadata IPv6 address", async () => { + const providerFetch = await loadFetch(false); + await expect( + providerFetch("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 () => { + const providerFetch = await loadFetch(false); + for (const address of [ + "[fe80::1]", + "[fe90::1]", + "[fea0::1]", + "[febf::1]", + ]) { + await expect( + providerFetch(`http://${address}/`), + `expected ${address} to be rejected`, + ).rejects.toThrow(/metadata/i); + } + expect(global.fetch).not.toHaveBeenCalled(); + }); + + it("does not block a hostname just because it starts like a link-local prefix", async () => { + const providerFetch = await loadFetch(false); + + await providerFetch("https://feature-logs.example.com/ready"); + expect(global.fetch).toHaveBeenCalledTimes(1); + }); + + it("rejects the GCP metadata hostname without needing DNS resolution", async () => { + const providerFetch = await loadFetch(false); + await expect( + providerFetch("http://metadata.google.internal/computeMetadata/v1/"), + ).rejects.toThrow(/metadata/i); + expect(global.fetch).not.toHaveBeenCalled(); + expect(mocks.lookup).not.toHaveBeenCalled(); + }); + + it("rejects loopback and private-network endpoints in cloud", async () => { + const providerFetch = await loadFetch(true); + + for (const url of [ + "http://10.0.0.5:3100/ready", + "http://127.0.0.1:3100/ready", + "http://localhost:3100/ready", + "http://[::1]:3100/ready", + "http://0.0.0.0:3100/ready", + "http://100.100.100.200/latest/meta-data/", + "http://[::]:3100/ready", + ]) { + await expect( + providerFetch(url), + `expected ${url} to be rejected`, + ).rejects.toThrow(/blocked/i); + } + expect(global.fetch).not.toHaveBeenCalled(); + }); + + it("does not follow redirects", async () => { + const providerFetch = await loadFetch(false); + + await providerFetch("https://logs.example.com/ready"); + expect(global.fetch).toHaveBeenCalledWith( + "https://logs.example.com/ready", + expect.objectContaining({ redirect: "error" }), + ); + }); +}); + +describe("providerFetch — address checked at connect time", () => { + let server: Server; + let port: number; + let hostHeader: string | undefined; + + beforeEach(async () => { + mocks.lookup.mockReset(); + hostHeader = undefined; + server = createServer((req, res) => { + hostHeader = req.headers.host; + res.end("ok"); + }); + await new Promise((resolve) => + server.listen(0, "127.0.0.1", resolve), + ); + port = (server.address() as AddressInfo).port; + }); + + afterEach(async () => { + await new Promise((resolve) => server.close(resolve)); + }); + + it("rejects a hostname whose lookup returns a metadata address", async () => { + const providerFetch = await loadFetch(false); + resolvesTo("203.0.113.10", "169.254.169.254"); + + await expect( + providerFetch("http://attacker-controlled.example.com/"), + ).rejects.toThrow(/metadata/i); + expect(mocks.lookup).toHaveBeenCalledWith( + "attacker-controlled.example.com", + expect.anything(), + expect.any(Function), + ); + }); + + it("rejects a hostname resolving to a private address in cloud", async () => { + const providerFetch = await loadFetch(true); + resolvesTo("127.0.0.1"); + + await expect( + providerFetch(`http://loki.internal.example.com:${port}/ready`), + ).rejects.toThrow(/blocked/i); + expect(hostHeader).toBeUndefined(); + }); + + it("rejects when DNS resolution fails", async () => { + const providerFetch = await loadFetch(false); + mocks.lookup.mockImplementation( + (_host: string, _options: unknown, callback: Function) => + callback(Object.assign(new Error("ENOTFOUND"), { code: "ENOTFOUND" })), + ); + + await expect( + providerFetch("http://does-not-exist.invalid/"), + ).rejects.toThrow(); + expect(hostHeader).toBeUndefined(); + }); + + it("connects to the address it validated, keeping the original Host header (self-hosted private endpoint)", async () => { + const providerFetch = await loadFetch(false); + resolvesTo("127.0.0.1"); + + const response = await providerFetch( + `http://loki.internal.example.com:${port}/ready`, + ); + expect(await response.text()).toBe("ok"); + expect(hostHeader).toBe(`loki.internal.example.com:${port}`); + }); + + it("passes a public address through in cloud", async () => { + const { guardedLookup } = await loadModule(true); + resolvesTo("203.0.113.10"); + + const result = await new Promise((resolve, reject) => + guardedLookup("api.datadoghq.com", { all: true }, (error, addresses) => + error ? reject(error) : resolve(addresses), + ), + ); + expect(result).toEqual([{ address: "203.0.113.10", family: 4 }]); + }); +}); diff --git a/apps/dokploy/__test__/logs-and-metrics/provider-label.test.ts b/apps/dokploy/__test__/logs-and-metrics/provider-label.test.ts new file mode 100644 index 000000000..30d84a6f8 --- /dev/null +++ b/apps/dokploy/__test__/logs-and-metrics/provider-label.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from "vitest"; +import { providerLabel } from "@/components/dashboard/settings/logs-and-metrics/show-servers"; + +describe("providerLabel", () => { + it("names what the provider sends next to its name", () => { + expect(providerLabel({ name: "Loki prod", signals: ["logs"] })).toBe( + "Loki prod (logs)", + ); + expect(providerLabel({ name: "Splunk", signals: ["metrics"] })).toBe( + "Splunk (metrics)", + ); + expect( + providerLabel({ name: "Datadog", signals: ["logs", "metrics"] }), + ).toBe("Datadog (logs and metrics)"); + }); +}); diff --git a/apps/dokploy/__test__/logs-and-metrics/remove-telemetry-provider.test.ts b/apps/dokploy/__test__/logs-and-metrics/remove-telemetry-provider.test.ts new file mode 100644 index 000000000..ad6cd1da4 --- /dev/null +++ b/apps/dokploy/__test__/logs-and-metrics/remove-telemetry-provider.test.ts @@ -0,0 +1,101 @@ +import type { SQL } from "drizzle-orm"; +import { PgDialect } from "drizzle-orm/pg-core"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + deleted: [] as unknown[], + updates: [] as Array<{ table: unknown; values: Record }>, + updatedServers: [] as Array<{ serverId: string }>, + updatedLocal: [] as Array<{ id: string }>, + queried: vi.fn(), +})); + +vi.mock("@dokploy/server/db", () => { + const tx = { + delete: () => ({ + where: () => ({ + returning: () => Promise.resolve(mocks.deleted), + }), + }), + update: (table: unknown) => ({ + set: (values: Record) => ({ + where: () => ({ + returning: () => { + mocks.updates.push({ table, values }); + return Promise.resolve( + mocks.updates.length === 1 + ? mocks.updatedServers + : mocks.updatedLocal, + ); + }, + }), + }), + }), + }; + return { + db: { + transaction: (fn: (tx: unknown) => unknown) => fn(tx), + query: { + server: { findMany: mocks.queried }, + webServerSettings: { findFirst: mocks.queried }, + }, + }, + }; +}); + +const { server, webServerSettings } = await import("@dokploy/server/db/schema"); +const { removeTelemetryProvider } = await import( + "@dokploy/server/services/logs-and-metrics/service" +); + +const render = (value: unknown) => new PgDialect().sqlToQuery(value as SQL).sql; + +describe("removeTelemetryProvider", () => { + beforeEach(() => { + mocks.updates = []; + mocks.updatedServers = [{ serverId: "server-1" }]; + mocks.updatedLocal = [{ id: "ws-1" }]; + mocks.queried.mockReset(); + }); + + it("drops the provider id from the selection of every server and of the local host", async () => { + mocks.deleted = [{ telemetryProviderId: "lp-1" }]; + + const { provider, targets } = await removeTelemetryProvider("lp-1"); + expect(provider).toEqual({ telemetryProviderId: "lp-1" }); + expect(targets).toEqual(["server-1", null]); + + expect(mocks.updates.map((u) => u.table)).toEqual([ + server, + webServerSettings, + ]); + expect(render(mocks.updates[0]?.values.telemetryProviderIds)).toBe( + 'array_remove("server"."telemetryProviderIds", $1)', + ); + expect(render(mocks.updates[1]?.values.telemetryProviderIds)).toBe( + 'array_remove("webServerSettings"."telemetryProviderIds", $1)', + ); + expect(render(mocks.updates[1]?.values.vectorAgentOrganizationId)).toBe( + 'CASE WHEN cardinality(array_remove("webServerSettings"."telemetryProviderIds", $1)) = 0 THEN NULL ELSE "webServerSettings"."vectorAgentOrganizationId" END', + ); + }); + + it("reconciles exactly the hosts whose selection the transaction updated", async () => { + mocks.deleted = [{ telemetryProviderId: "lp-1" }]; + mocks.updatedServers = [{ serverId: "server-1" }, { serverId: "server-2" }]; + mocks.updatedLocal = []; + + const { targets } = await removeTelemetryProvider("lp-1"); + expect(targets).toEqual(["server-1", "server-2"]); + expect(mocks.queried).not.toHaveBeenCalled(); + }); + + it("does not touch assignments when the provider does not exist", async () => { + mocks.deleted = []; + + await expect(removeTelemetryProvider("missing")).rejects.toMatchObject({ + code: "NOT_FOUND", + }); + expect(mocks.updates).toEqual([]); + }); +}); diff --git a/apps/dokploy/__test__/logs-and-metrics/telemetry-provider-adapters.test.ts b/apps/dokploy/__test__/logs-and-metrics/telemetry-provider-adapters.test.ts new file mode 100644 index 000000000..31b01a09b --- /dev/null +++ b/apps/dokploy/__test__/logs-and-metrics/telemetry-provider-adapters.test.ts @@ -0,0 +1,392 @@ +import { + getTelemetryProviderAdapter, + telemetryProviderAdapters, +} from "@dokploy/server/services/logs-and-metrics/providers/registry"; +import type { TelemetryProviderRuntimeConfig } from "@dokploy/server/services/logs-and-metrics/types"; +import { describe, expect, it } from "vitest"; + +const baseConfig: TelemetryProviderRuntimeConfig = { + telemetryProviderId: "provider-1", + name: "test", + signals: ["logs"], + endpoint: null, + apiKey: null, + apiSecret: null, + extraConfig: null, +}; + +describe("getTelemetryProviderAdapter", () => { + it("returns the registered adapter for each known type", () => { + expect(getTelemetryProviderAdapter("loki").type).toBe("loki"); + expect(getTelemetryProviderAdapter("datadog").type).toBe("datadog"); + expect(getTelemetryProviderAdapter("betterstack").type).toBe("betterstack"); + expect(getTelemetryProviderAdapter("elasticsearch").type).toBe( + "elasticsearch", + ); + expect(getTelemetryProviderAdapter("splunk_hec").type).toBe("splunk_hec"); + expect(getTelemetryProviderAdapter("aws_cloudwatch").type).toBe( + "aws_cloudwatch", + ); + }); + + it("throws an explicit error for an unregistered type", () => { + // @ts-expect-error: intentionally an unregistered type + expect(() => getTelemetryProviderAdapter("unknown")).toThrow( + /No TelemetryProviderAdapter registered/, + ); + }); +}); + +describe("lokiAdapter.toVectorSink", () => { + it("maps endpoint + labels, without tenant_id if not set", () => { + const adapter = telemetryProviderAdapters.loki; + const sink = adapter.toVectorSink( + { ...baseConfig, endpoint: "https://loki.example.com" }, + "sink_1", + "dokploy_scope", + "logs", + ); + 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_environment: "{{ dokploy_environment }}", + dokploy_application: "{{ dokploy_application }}", + dokploy_organization: "{{ dokploy_organization }}", + dokploy_server: "{{ dokploy_server }}", + }); + 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 = telemetryProviderAdapters.loki; + const sink = adapter.toVectorSink( + { + ...baseConfig, + endpoint: "https://loki.example.com", + extraConfig: { tenantId: "tenant-a" }, + }, + "sink_1", + "dokploy_scope", + "logs", + ); + expect(sink.tenant_id).toBe("tenant-a"); + }); + + it("has no toVectorTransform — goes directly scope -> sink", () => { + expect(telemetryProviderAdapters.loki.toVectorTransform).toBeUndefined(); + }); +}); + +describe("datadogAdapter.toVectorSink", () => { + it("defaults site to datadoghq.com and sets no `tags` field (datadog_logs has none)", () => { + const adapter = telemetryProviderAdapters.datadog; + const sink = adapter.toVectorSink( + { ...baseConfig, apiKey: "dd-key" }, + "sink_2", + "transform_2", + "logs", + ); + 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 = telemetryProviderAdapters.datadog; + const sink = adapter.toVectorSink( + { + ...baseConfig, + apiKey: "dd-key", + extraConfig: { site: "datadoghq.eu" }, + }, + "sink_2", + "transform_2", + "logs", + ); + expect(sink.site).toBe("datadoghq.eu"); + }); + + it("strips a pasted scheme/trailing slash from extraConfig.site instead of interpolating it as-is", () => { + const adapter = telemetryProviderAdapters.datadog; + const sink = adapter.toVectorSink( + { + ...baseConfig, + apiKey: "dd-key", + extraConfig: { site: "https://datadoghq.eu/" }, + }, + "sink_2", + "transform_2", + "logs", + ); + expect(sink.site).toBe("datadoghq.eu"); + }); + + it("builds .ddtags from the scoping fields in toVectorTransform, since datadog_logs has no `tags` option (vector generate-schema)", () => { + const adapter = telemetryProviderAdapters.datadog; + expect(adapter.toVectorTransform).toBeDefined(); + const transform = adapter.toVectorTransform?.( + baseConfig, + "transform_2", + "dokploy_scope", + "logs", + ); + expect(transform?.type).toBe("remap"); + expect(transform?.inputs).toEqual(["dokploy_scope"]); + expect(transform?.source).toContain(".ddtags"); + expect(transform?.source).toContain( + '"dokploy_server:" + replace(to_string!(.dokploy_server), ",", "_")', + ); + }); + + it("escapes commas in scoping field values before building .ddtags, since ddtags is itself comma delimited", () => { + const adapter = telemetryProviderAdapters.datadog; + const transform = adapter.toVectorTransform?.( + baseConfig, + "transform_2", + "dokploy_scope", + "logs", + ); + expect(transform?.source).toContain( + 'replace(to_string!(.dokploy_project), ",", "_")', + ); + }); +}); + +describe("betterStackAdapter", () => { + it("toVectorTransform renames timestamp to dt, inputs point to scope transform", () => { + const adapter = telemetryProviderAdapters.betterstack; + expect(adapter.toVectorTransform).toBeDefined(); + const transform = adapter.toVectorTransform?.( + baseConfig, + "transform_1", + "dokploy_scope", + "logs", + ); + 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 = telemetryProviderAdapters.betterstack; + const sink = adapter.toVectorSink( + { + ...baseConfig, + endpoint: "https://in.logs.betterstack.com", + apiKey: "source-token", + }, + "sink_3", + "transform_1", + "logs", + ); + 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 = telemetryProviderAdapters.elasticsearch; + expect(adapter.toVectorTransform).toBeDefined(); + const transform = adapter.toVectorTransform?.( + baseConfig, + "transform_4", + "dokploy_scope", + "logs", + ); + 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 = telemetryProviderAdapters.elasticsearch; + const sink = adapter.toVectorSink( + { + ...baseConfig, + endpoint: "https://es.example.com:9200", + apiKey: "secret", + extraConfig: { username: "elastic" }, + }, + "sink_4", + "dokploy_scope", + "logs", + ); + 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 = telemetryProviderAdapters.elasticsearch; + const sink = adapter.toVectorSink( + { + ...baseConfig, + endpoint: "https://es.example.com:9200", + apiKey: "key-123", + }, + "sink_4", + "dokploy_scope", + "logs", + ); + 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 = telemetryProviderAdapters.elasticsearch; + expect(() => + adapter.toVectorSink( + { + ...baseConfig, + endpoint: "https://es.example.com:9200", + extraConfig: { username: "elastic" }, + }, + "sink_4", + "dokploy_scope", + "logs", + ), + ).toThrow(/Password.*required/i); + }); + + it("includes bulk.index only when extraConfig.index is set", () => { + const adapter = telemetryProviderAdapters.elasticsearch; + const withoutIndex = adapter.toVectorSink( + { ...baseConfig, endpoint: "https://es.example.com:9200" }, + "sink_4", + "dokploy_scope", + "logs", + ); + 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", + "logs", + ); + expect(withIndex.bulk).toEqual({ index: "dokploy-%Y.%m.%d" }); + }); +}); + +describe("splunkAdapter", () => { + it("has no toVectorTransform — scoping fields ride along as plain JSON fields", () => { + expect( + telemetryProviderAdapters.splunk_hec.toVectorTransform, + ).toBeUndefined(); + }); + + it("toVectorSink maps endpoint/token, only sets index/sourcetype when present", () => { + const adapter = telemetryProviderAdapters.splunk_hec; + const sink = adapter.toVectorSink( + { + ...baseConfig, + endpoint: "https://splunk.example.com:8088", + apiKey: "hec-token", + }, + "sink_5", + "dokploy_scope", + "logs", + ); + 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", + "logs", + ); + expect(withExtras.index).toBe("dokploy"); + expect(withExtras.sourcetype).toBe("docker"); + }); +}); + +describe("awsCloudwatchAdapter", () => { + it("has no logs transform — scoping fields ride along as plain JSON fields", () => { + expect( + telemetryProviderAdapters.aws_cloudwatch.toVectorTransform?.( + baseConfig, + "transform_1", + "dokploy_scope", + "logs", + ), + ).toBeNull(); + }); + + it("toVectorSink maps access key/secret, region and log group/stream", () => { + const adapter = telemetryProviderAdapters.aws_cloudwatch; + const sink = adapter.toVectorSink( + { + ...baseConfig, + apiKey: "AKIA...", + apiSecret: "shh", + extraConfig: { region: "us-east-1", logGroup: "/dokploy/logs" }, + }, + "sink_6", + "dokploy_scope", + "logs", + ); + 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 = telemetryProviderAdapters.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", + "logs", + ); + expect(sink.stream_name).toBe("{{ dokploy_application }}"); + }); +}); diff --git a/apps/dokploy/__test__/logs-and-metrics/telemetry-provider-service.test.ts b/apps/dokploy/__test__/logs-and-metrics/telemetry-provider-service.test.ts new file mode 100644 index 000000000..e30efb520 --- /dev/null +++ b/apps/dokploy/__test__/logs-and-metrics/telemetry-provider-service.test.ts @@ -0,0 +1,661 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + insertValues: vi.fn(), + updateSet: vi.fn(), + findFirst: vi.fn(), + findMany: vi.fn(), + serverFindFirst: vi.fn(), + serverFindMany: vi.fn(), + webServerFindFirst: 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: { + telemetryProvider: { + findFirst: mocks.findFirst, + findMany: mocks.findMany, + }, + server: { + findFirst: mocks.serverFindFirst, + findMany: mocks.serverFindMany, + }, + webServerSettings: { findFirst: mocks.webServerFindFirst }, + }, + }, +})); + +const { + assertProvidersBelongToOrg, + assertSignalsSupported, + createTelemetryProvider, + testTelemetryProviderConnection, + updateTelemetryProvider, +} = await import("@dokploy/server/services/logs-and-metrics/service"); +const { telemetryProviderAdapters } = await import( + "@dokploy/server/services/logs-and-metrics/providers/registry" +); + +beforeEach(() => { + mocks.serverFindMany.mockResolvedValue([]); + mocks.webServerFindFirst.mockResolvedValue(undefined); +}); + +describe("updateTelemetryProvider — targets to re-apply", () => { + const existing = { + telemetryProviderId: "lp-1", + name: "loki", + providerType: "loki", + signals: ["logs"], + endpoint: "https://loki.example.com", + apiKey: "old", + apiSecret: null, + extraConfig: null, + enabled: true, + }; + + beforeEach(() => { + mocks.findFirst.mockResolvedValue(existing); + mocks.updateSet.mockReturnValue([{ ...existing, apiKey: "new" }]); + mocks.serverFindMany.mockResolvedValue([{ serverId: "server-1" }]); + mocks.webServerFindFirst.mockResolvedValue({ id: "ws-1" }); + }); + + it("returns the servers and the local host that ship with it when a credential changes", async () => { + const { targets } = await updateTelemetryProvider("lp-1", { + apiKey: "new", + }); + expect(targets).toEqual(["server-1", null]); + }); + + it("returns the same targets when the provider is disabled", async () => { + const { targets } = await updateTelemetryProvider("lp-1", { + enabled: false, + }); + expect(targets).toEqual(["server-1", null]); + }); + + it("returns no targets when only the name changes or a value is re-sent unchanged", async () => { + expect( + (await updateTelemetryProvider("lp-1", { name: "renamed" })).targets, + ).toEqual([]); + expect( + ( + await updateTelemetryProvider("lp-1", { + endpoint: "https://loki.example.com", + }) + ).targets, + ).toEqual([]); + }); +}); + +describe("createTelemetryProvider — required credential fields per adapter", () => { + it("rejects a loki provider without endpoint", async () => { + await expect( + createTelemetryProvider( + { name: "loki-prod", providerType: "loki", signals: ["logs"] } as any, + "org-1", + ), + ).rejects.toThrow(/endpoint/i); + expect(mocks.insertValues).not.toHaveBeenCalled(); + }); + + it("rejects a datadog provider without apiKey", async () => { + await expect( + createTelemetryProvider( + { name: "dd", providerType: "datadog", signals: ["logs"] } as any, + "org-1", + ), + ).rejects.toThrow(); + expect(mocks.insertValues).not.toHaveBeenCalled(); + }); + + it("rejects a betterstack provider missing either field", async () => { + await expect( + createTelemetryProvider( + { + name: "bs", + providerType: "betterstack", + signals: ["logs"], + 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( + createTelemetryProvider( + { + name: "splunk", + providerType: "splunk_hec", + signals: ["logs"], + 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( + createTelemetryProvider( + { + name: "cw", + providerType: "aws_cloudwatch", + signals: ["logs"], + 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( + createTelemetryProvider( + { + name: "dd", + providerType: "datadog", + signals: ["logs"], + 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( + createTelemetryProvider( + { + name: "es", + providerType: "elasticsearch", + signals: ["logs"], + 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([ + { + telemetryProviderId: "lp-2", + name: "cw", + providerType: "aws_cloudwatch", + signals: ["logs"], + }, + ]); + const created = await createTelemetryProvider( + { + name: "cw", + providerType: "aws_cloudwatch", + signals: ["logs"], + apiKey: "AKIA...", + apiSecret: "shh", + extraConfig: { region: "us-east-1", logGroup: "/dokploy/logs" }, + } as any, + "org-1", + ); + expect(created.telemetryProviderId).toBe("lp-2"); + }); + + it("accepts a loki provider with endpoint", async () => { + mocks.insertValues.mockReturnValue([ + { telemetryProviderId: "lp-1", name: "loki-prod", providerType: "loki" }, + ]); + const created = await createTelemetryProvider( + { + name: "loki-prod", + providerType: "loki", + signals: ["logs"], + endpoint: "https://loki.example.com", + } as any, + "org-1", + ); + expect(created.telemetryProviderId).toBe("lp-1"); + expect(mocks.insertValues).toHaveBeenCalled(); + }); +}); + +describe("updateTelemetryProvider — merges partial update with existing row before validating", () => { + it("allows updating just the name without re-sending existing credentials", async () => { + mocks.findFirst.mockResolvedValue({ + telemetryProviderId: "lp-1", + name: "old-name", + providerType: "loki", + signals: ["logs"], + endpoint: "https://loki.example.com", + apiKey: null, + apiSecret: null, + extraConfig: null, + }); + mocks.updateSet.mockReturnValue([ + { telemetryProviderId: "lp-1", name: "new-name" }, + ]); + + const { provider: updated, targets } = await updateTelemetryProvider( + "lp-1", + { name: "new-name" }, + ); + expect(updated.name).toBe("new-name"); + expect(targets).toEqual([]); + }); + + it("rejects clearing the only required field via update", async () => { + mocks.findFirst.mockResolvedValue({ + telemetryProviderId: "lp-1", + name: "loki-prod", + providerType: "loki", + signals: ["logs"], + endpoint: "https://loki.example.com", + apiKey: null, + apiSecret: null, + extraConfig: null, + }); + + await expect( + updateTelemetryProvider("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({ + telemetryProviderId: "lp-1", + name: "dd-prod", + providerType: "datadog", + signals: ["logs"], + endpoint: null, + apiKey: "old-datadog-key", + apiSecret: null, + extraConfig: { site: "datadoghq.com" }, + }); + mocks.updateSet.mockReturnValue([{ telemetryProviderId: "lp-1" }]); + + await updateTelemetryProvider("lp-1", { + providerType: "loki", + signals: ["logs"], + endpoint: "https://loki.example.com", + }); + + expect(mocks.updateSet).toHaveBeenCalledWith( + expect.objectContaining({ + providerType: "loki", + signals: ["logs"], + 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({ + telemetryProviderId: "lp-1", + name: "es-prod", + providerType: "elasticsearch", + signals: ["logs"], + endpoint: "https://es.example.com:9200", + apiKey: "secret", + apiSecret: null, + extraConfig: { username: "elastic", index: "custom-index" }, + }); + mocks.updateSet.mockReturnValue([{ telemetryProviderId: "lp-1" }]); + + await updateTelemetryProvider("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({ + telemetryProviderId: "lp-1", + name: "es-prod", + providerType: "elasticsearch", + signals: ["logs"], + endpoint: "https://es.example.com:9200", + apiKey: null, + apiSecret: null, + extraConfig: { username: "elastic" }, + }); + mocks.updateSet.mockReturnValue([{ telemetryProviderId: "lp-1" }]); + + await updateTelemetryProvider("lp-1", { extraConfig: null }); + + expect(mocks.updateSet).toHaveBeenCalledWith( + expect.objectContaining({ extraConfig: null }), + ); + }); + + it("clears an optional credential when it is explicitly set to null", async () => { + mocks.findFirst.mockResolvedValue({ + telemetryProviderId: "lp-1", + name: "loki-prod", + providerType: "loki", + signals: ["logs"], + endpoint: "https://loki.example.com", + apiKey: "old-token", + apiSecret: null, + extraConfig: { username: "user" }, + }); + mocks.updateSet.mockReturnValue([{ telemetryProviderId: "lp-1" }]); + + await updateTelemetryProvider("lp-1", { apiKey: null }); + + expect(mocks.updateSet).toHaveBeenCalledWith( + expect.objectContaining({ apiKey: null }), + ); + }); + + it("rejects clearing the elasticsearch password while a username stays saved", async () => { + mocks.findFirst.mockResolvedValue({ + telemetryProviderId: "lp-1", + name: "es-prod", + providerType: "elasticsearch", + signals: ["logs"], + endpoint: "https://es.example.com:9200", + apiKey: "old-password", + apiSecret: null, + extraConfig: { username: "elastic" }, + }); + + await expect( + updateTelemetryProvider("lp-1", { apiKey: null }), + ).rejects.toThrow(/Password/); + }); + + it("rejects a providerType change that doesn't satisfy the new type's required fields", async () => { + mocks.findFirst.mockResolvedValue({ + telemetryProviderId: "lp-1", + name: "dd-prod", + providerType: "datadog", + signals: ["logs"], + endpoint: null, + apiKey: "old-datadog-key", + apiSecret: null, + extraConfig: null, + }); + + await expect( + updateTelemetryProvider("lp-1", { providerType: "loki" }), + ).rejects.toThrow(/endpoint/i); + }); +}); + +describe("assertSignalsSupported", () => { + it("rejects a repeated signal", () => { + expect(() => assertSignalsSupported("loki", ["logs", "logs"])).toThrow( + /only be selected once/, + ); + }); + + it("rejects a signal the type cannot send", () => { + expect(() => assertSignalsSupported("loki", ["metrics"])).toThrow( + /Grafana Loki cannot send metrics/, + ); + }); + + it("accepts a supported subset", () => { + expect(() => assertSignalsSupported("loki", ["logs"])).not.toThrow(); + }); +}); + +describe("createTelemetryProvider — signals", () => { + it("rejects a provider whose type cannot send the selected signal", async () => { + mocks.insertValues.mockClear(); + await expect( + createTelemetryProvider( + { + name: "loki", + providerType: "loki", + signals: ["metrics"], + endpoint: "https://loki.example.com", + } as any, + "org-1", + ), + ).rejects.toThrow(/cannot send metrics/); + expect(mocks.insertValues).not.toHaveBeenCalled(); + }); +}); + +describe("assertProvidersBelongToOrg", () => { + beforeEach(() => { + mocks.findMany.mockResolvedValue([ + { + telemetryProviderId: "lp-1", + name: "loki", + enabled: true, + signals: ["logs"], + }, + ]); + }); + + it("accepts a provider of the organization regardless of what it sends", async () => { + await expect( + assertProvidersBelongToOrg(["lp-1"], "org-1"), + ).resolves.toBeUndefined(); + }); + + it("rejects an id that is not in the organization", async () => { + await expect( + assertProvidersBelongToOrg(["lp-9"], "org-1"), + ).rejects.toMatchObject({ code: "UNAUTHORIZED" }); + }); + + it("rejects a selection where every provider is disabled", async () => { + mocks.findMany.mockResolvedValue([ + { + telemetryProviderId: "lp-1", + name: "loki", + enabled: false, + signals: ["logs"], + }, + ]); + await expect(assertProvidersBelongToOrg(["lp-1"], "org-1")).rejects.toThrow( + /at least one enabled provider/, + ); + }); +}); + +describe("required fields scoped to a signal", () => { + beforeEach(() => { + mocks.insertValues.mockReset(); + mocks.insertValues.mockReturnValue([{ telemetryProviderId: "cw-1" }]); + }); + + it("does not require the log group when only metrics are selected", async () => { + await expect( + createTelemetryProvider( + { + name: "cw", + providerType: "aws_cloudwatch", + signals: ["metrics"], + apiKey: "AKIA", + apiSecret: "shh", + extraConfig: { region: "us-east-1", metricsNamespace: "Dokploy" }, + } as any, + "org-1", + ), + ).resolves.toMatchObject({ telemetryProviderId: "cw-1" }); + }); + + it("does not require the metrics namespace when only logs are selected", async () => { + await expect( + createTelemetryProvider( + { + name: "cw", + providerType: "aws_cloudwatch", + signals: ["logs"], + apiKey: "AKIA", + apiSecret: "shh", + extraConfig: { region: "us-east-1", logGroup: "/dokploy/logs" }, + } as any, + "org-1", + ), + ).resolves.toMatchObject({ telemetryProviderId: "cw-1" }); + }); + + it("requires the metrics namespace when metrics are selected", async () => { + await expect( + createTelemetryProvider( + { + name: "cw", + providerType: "aws_cloudwatch", + signals: ["logs", "metrics"], + apiKey: "AKIA", + apiSecret: "shh", + extraConfig: { region: "us-east-1", logGroup: "/dokploy/logs" }, + } as any, + "org-1", + ), + ).rejects.toThrow(/Metrics namespace/); + expect(mocks.insertValues).not.toHaveBeenCalled(); + }); +}); + +describe("signals are stored in a fixed order", () => { + it("normalizes the order on create", async () => { + mocks.insertValues.mockReset(); + mocks.insertValues.mockReturnValue([{ telemetryProviderId: "dd-1" }]); + + await createTelemetryProvider( + { + name: "dd", + providerType: "datadog", + signals: ["metrics", "logs"], + apiKey: "dd-key", + } as any, + "org-1", + ); + + expect(mocks.insertValues).toHaveBeenCalledWith( + expect.objectContaining({ signals: ["logs", "metrics"] }), + ); + }); + + it("normalizes the order on update", async () => { + mocks.findFirst.mockResolvedValue({ + telemetryProviderId: "dd-1", + name: "dd", + providerType: "datadog", + signals: ["logs"], + endpoint: null, + apiKey: "dd-key", + apiSecret: null, + extraConfig: null, + }); + mocks.updateSet.mockReturnValue([{ telemetryProviderId: "dd-1" }]); + + await updateTelemetryProvider("dd-1", { signals: ["metrics", "logs"] }); + + expect(mocks.updateSet).toHaveBeenCalledWith( + expect.objectContaining({ signals: ["logs", "metrics"] }), + ); + }); +}); + +describe("testTelemetryProviderConnection by id", () => { + it("tests the saved provider with the signals override from the form", async () => { + mocks.findFirst.mockResolvedValue({ + telemetryProviderId: "bs-1", + name: "bs", + providerType: "betterstack", + signals: ["logs"], + endpoint: "in.logs.betterstack.com", + apiKey: "token", + apiSecret: null, + extraConfig: null, + }); + const spy = vi + .spyOn(telemetryProviderAdapters.betterstack, "testConnection") + .mockResolvedValue(undefined); + + await testTelemetryProviderConnection({ + telemetryProviderId: "bs-1", + signals: ["logs", "metrics"], + }); + + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + apiKey: "token", + signals: ["logs", "metrics"], + }), + ); + spy.mockRestore(); + }); +}); + +describe("updateTelemetryProvider — signals", () => { + const existing = { + telemetryProviderId: "dd-1", + name: "datadog", + providerType: "datadog", + signals: ["logs", "metrics"], + endpoint: null, + apiKey: "dd-key", + apiSecret: null, + extraConfig: null, + }; + + beforeEach(() => { + mocks.updateSet.mockReset(); + mocks.serverFindFirst.mockReset(); + mocks.webServerFindFirst.mockReset(); + mocks.findFirst.mockResolvedValue(existing); + mocks.updateSet.mockReturnValue([{ ...existing, signals: ["metrics"] }]); + }); + + it("allows removing a signal while the provider is assigned and returns its targets to re-apply", async () => { + mocks.serverFindMany.mockResolvedValue([{ serverId: "server-1" }]); + mocks.webServerFindFirst.mockResolvedValue({ id: "ws-1" }); + + await expect( + updateTelemetryProvider("dd-1", { signals: ["metrics"] }), + ).resolves.toMatchObject({ + provider: { signals: ["metrics"] }, + targets: ["server-1", null], + }); + expect(mocks.updateSet).toHaveBeenCalledWith( + expect.objectContaining({ signals: ["metrics"] }), + ); + }); + + it("rejects a type change that cannot send the saved signals", async () => { + await expect( + updateTelemetryProvider("dd-1", { + providerType: "loki", + endpoint: "https://loki.example.com", + }), + ).rejects.toThrow(/Grafana Loki cannot send metrics/); + }); +}); diff --git a/apps/dokploy/__test__/logs-and-metrics/vector-agent.test.ts b/apps/dokploy/__test__/logs-and-metrics/vector-agent.test.ts new file mode 100644 index 000000000..6cc14b1c5 --- /dev/null +++ b/apps/dokploy/__test__/logs-and-metrics/vector-agent.test.ts @@ -0,0 +1,305 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + setWebServerProviderIds: vi.fn(), + setServerProviderIds: vi.fn(), + getAccessibleServerIds: vi.fn(), + reconcileVectorAgent: vi.fn(), + removeVectorAgent: vi.fn(), + findServerById: vi.fn(), + assertProvidersBelongToOrg: vi.fn(), +})); + +const loadAgent = async (isCloud: boolean) => { + vi.resetModules(); + vi.doMock("@dokploy/server/constants", () => ({ IS_CLOUD: isCloud })); + vi.doMock("@dokploy/server/services/server", () => ({ + findServerById: mocks.findServerById, + setServerProviderIds: mocks.setServerProviderIds, + getAccessibleServerIds: mocks.getAccessibleServerIds, + })); + vi.doMock("@dokploy/server/services/web-server-settings", () => ({ + setWebServerProviderIds: mocks.setWebServerProviderIds, + })); + const locks = new Map>(); + vi.doMock("@dokploy/server/setup/vector-setup", () => ({ + LOCAL_SERVER_NAME: "Dokploy Server (local)", + reconcileVectorAgent: mocks.reconcileVectorAgent, + removeVectorAgent: mocks.removeVectorAgent, + withVectorTargetLock: ( + serverId: string | undefined, + fn: () => Promise, + ) => { + const key = serverId ?? "local"; + const current = (locks.get(key) ?? Promise.resolve()) + .catch(() => {}) + .then(fn); + locks.set(key, current); + return current; + }, + })); + vi.doMock("@dokploy/server/services/logs-and-metrics/service", () => ({ + assertProvidersBelongToOrg: mocks.assertProvidersBelongToOrg, + })); + return await import("@dokploy/server/services/logs-and-metrics/vector-agent"); +}; + +const sessionA = { userId: "user-a", activeOrganizationId: "org-a" }; +const sessionB = { userId: "user-b", activeOrganizationId: "org-b" }; + +beforeEach(() => { + for (const mock of Object.values(mocks)) mock.mockReset(); + mocks.getAccessibleServerIds.mockResolvedValue(new Set(["server-1"])); + mocks.findServerById.mockImplementation(async (serverId: string) => ({ + serverId, + name: `name-of-${serverId}`, + organizationId: "org-a", + sshKeyId: "key-1", + })); + mocks.setWebServerProviderIds.mockResolvedValue({ id: "ws-1" }); + mocks.setServerProviderIds.mockResolvedValue({ serverId: "server-1" }); +}); + +describe("applyVectorAgentSelection on the local host", () => { + it("rejects the local host in cloud without touching the database", async () => { + const { applyVectorAgentSelection } = await loadAgent(true); + + await expect( + applyVectorAgentSelection(sessionA, undefined, ["lp-1"]), + ).rejects.toMatchObject({ code: "BAD_REQUEST" }); + expect(mocks.setWebServerProviderIds).not.toHaveBeenCalled(); + expect(mocks.reconcileVectorAgent).not.toHaveBeenCalled(); + }); + + it("rejects with CONFLICT when another organization owns the local agent, without reconciling", async () => { + mocks.setWebServerProviderIds.mockResolvedValue(null); + const { applyVectorAgentSelection } = await loadAgent(false); + + await expect( + applyVectorAgentSelection(sessionB, undefined, ["mp-1"]), + ).rejects.toMatchObject({ code: "CONFLICT" }); + expect(mocks.setWebServerProviderIds).toHaveBeenCalledWith("org-b", [ + "mp-1", + ]); + expect(mocks.reconcileVectorAgent).not.toHaveBeenCalled(); + }); + + it("saves the selection and then reconciles", async () => { + const { applyVectorAgentSelection } = await loadAgent(false); + + await applyVectorAgentSelection(sessionA, undefined, ["lp-1"]); + expect(mocks.assertProvidersBelongToOrg).toHaveBeenCalledWith( + ["lp-1"], + "org-a", + ); + expect(mocks.setWebServerProviderIds).toHaveBeenCalledWith("org-a", [ + "lp-1", + ]); + expect(mocks.reconcileVectorAgent).toHaveBeenCalledWith(undefined); + expect( + mocks.setWebServerProviderIds.mock.invocationCallOrder[0], + ).toBeLessThan(mocks.reconcileVectorAgent.mock.invocationCallOrder[0] ?? 0); + }); + + it("removing the agent saves an empty selection without checking providers", async () => { + const { applyVectorAgentSelection } = await loadAgent(false); + + await applyVectorAgentSelection(sessionA, undefined, []); + expect(mocks.assertProvidersBelongToOrg).not.toHaveBeenCalled(); + expect(mocks.setWebServerProviderIds).toHaveBeenCalledWith("org-a", []); + expect(mocks.reconcileVectorAgent).toHaveBeenCalledWith(undefined); + }); +}); + +describe("applyVectorAgentSelection on a remote server", () => { + it("still manages remote servers in cloud", async () => { + const { applyVectorAgentSelection } = await loadAgent(true); + + const result = await applyVectorAgentSelection(sessionA, "server-1", []); + expect(result).toEqual({ serverName: "name-of-server-1" }); + expect(mocks.setServerProviderIds).toHaveBeenCalledWith("server-1", []); + expect(mocks.reconcileVectorAgent).toHaveBeenCalledWith("server-1"); + }); + + it("rejects a server of another organization", async () => { + mocks.findServerById.mockResolvedValue({ + serverId: "server-1", + organizationId: "org-b", + sshKeyId: "key-1", + }); + const { applyVectorAgentSelection } = await loadAgent(false); + + await expect( + applyVectorAgentSelection(sessionA, "server-1", ["lp-1"]), + ).rejects.toMatchObject({ code: "UNAUTHORIZED" }); + expect(mocks.setServerProviderIds).not.toHaveBeenCalled(); + }); + + it("rejects a server without an SSH key before saving anything", async () => { + mocks.findServerById.mockResolvedValue({ + serverId: "server-1", + organizationId: "org-a", + sshKeyId: null, + }); + const { applyVectorAgentSelection } = await loadAgent(false); + + await expect( + applyVectorAgentSelection(sessionA, "server-1", ["lp-1"]), + ).rejects.toMatchObject({ code: "BAD_REQUEST" }); + expect(mocks.setServerProviderIds).not.toHaveBeenCalled(); + expect(mocks.reconcileVectorAgent).not.toHaveBeenCalled(); + }); + + it("rejects a server the member cannot access", async () => { + const { applyVectorAgentSelection } = await loadAgent(false); + + await expect( + applyVectorAgentSelection(sessionA, "server-2", ["lp-1"]), + ).rejects.toMatchObject({ code: "UNAUTHORIZED" }); + expect(mocks.getAccessibleServerIds).toHaveBeenCalledWith(sessionA); + expect(mocks.setServerProviderIds).not.toHaveBeenCalled(); + expect(mocks.reconcileVectorAgent).not.toHaveBeenCalled(); + }); + + it("saves the array and only then reconciles", async () => { + const { applyVectorAgentSelection } = await loadAgent(false); + + await applyVectorAgentSelection(sessionA, "server-1", ["mp-1"]); + expect(mocks.setServerProviderIds).toHaveBeenCalledWith("server-1", [ + "mp-1", + ]); + expect(mocks.setServerProviderIds.mock.invocationCallOrder[0]).toBeLessThan( + mocks.reconcileVectorAgent.mock.invocationCallOrder[0] ?? 0, + ); + }); + + it("keeps the saved selection when the reconcile fails", async () => { + mocks.reconcileVectorAgent.mockRejectedValue( + new Error("validation failed"), + ); + const { applyVectorAgentSelection } = await loadAgent(false); + + await expect( + applyVectorAgentSelection(sessionA, "server-1", ["lp-2"]), + ).rejects.toThrow("validation failed"); + expect(mocks.setServerProviderIds).toHaveBeenCalledWith("server-1", [ + "lp-2", + ]); + }); + + it("serializes concurrent selections on the same host so the last reconcile sees the last selection", async () => { + const saved: string[][] = []; + const seen: string[][] = []; + mocks.setServerProviderIds.mockImplementation( + async (_serverId: string, ids: string[]) => { + saved.push(ids); + return { serverId: "server-1" }; + }, + ); + mocks.reconcileVectorAgent.mockImplementation(async () => { + seen.push(saved.at(-1) ?? []); + await new Promise((resolve) => setTimeout(resolve, 20)); + }); + const { applyVectorAgentSelection } = await loadAgent(false); + + await Promise.all([ + applyVectorAgentSelection(sessionA, "server-1", ["lp-1"]), + applyVectorAgentSelection(sessionA, "server-1", ["lp-2"]), + ]); + expect(seen).toEqual([["lp-1"], ["lp-2"]]); + }); +}); + +describe("removeServerVectorAgent", () => { + it("removes the agent on server deletion when providers are assigned", async () => { + const { removeServerVectorAgent } = await loadAgent(false); + + await expect( + removeServerVectorAgent({ + serverId: "server-1", + sshKeyId: "key-1", + telemetryProviderIds: ["lp-1"], + } as never), + ).resolves.toBeUndefined(); + expect(mocks.removeVectorAgent).toHaveBeenCalledWith("server-1"); + }); + + it("attempts cleanup even with an empty selection, since a failed Remove can leave the agent running", async () => { + const { removeServerVectorAgent } = await loadAgent(false); + + await expect( + removeServerVectorAgent({ + serverId: "server-1", + sshKeyId: "key-1", + telemetryProviderIds: [], + } as never), + ).resolves.toBeUndefined(); + expect(mocks.removeVectorAgent).toHaveBeenCalledWith("server-1"); + }); + + it("skips servers without an SSH key, warning when providers were assigned", async () => { + const { removeServerVectorAgent } = await loadAgent(false); + + await expect( + removeServerVectorAgent({ + serverId: "server-1", + sshKeyId: null, + telemetryProviderIds: [], + } as never), + ).resolves.toBeUndefined(); + await expect( + removeServerVectorAgent({ + serverId: "server-1", + sshKeyId: null, + telemetryProviderIds: ["lp-1"], + } as never), + ).resolves.toMatch(/no SSH key/); + expect(mocks.removeVectorAgent).not.toHaveBeenCalled(); + }); + + it("returns the failure as a message instead of throwing", async () => { + mocks.removeVectorAgent.mockRejectedValue(new Error("docker down")); + const { removeServerVectorAgent } = await loadAgent(false); + + await expect( + removeServerVectorAgent({ + serverId: "server-1", + sshKeyId: "k", + telemetryProviderIds: ["lp-1"], + } as never), + ).resolves.toBe("docker down"); + }); +}); + +describe("reconcileVectorTargets", () => { + it("re-applies every target under its lock and reports failures as a warning", async () => { + mocks.reconcileVectorAgent.mockImplementation(async (serverId?: string) => { + if (serverId === "server-2") throw new Error("ssh down"); + }); + const { reconcileVectorTargets } = await loadAgent(false); + + const warning = await reconcileVectorTargets([ + "server-1", + "server-2", + null, + ]); + expect(mocks.reconcileVectorAgent.mock.calls.map((c) => c[0])).toEqual([ + "server-1", + "server-2", + undefined, + ]); + expect(warning).toBe( + "The Vector agent could not be re-applied on server-2: ssh down", + ); + }); + + it("returns no warning when every target reconciles, and skips the local host in cloud", async () => { + const { reconcileVectorTargets } = await loadAgent(true); + + await expect( + reconcileVectorTargets(["server-1", null]), + ).resolves.toBeUndefined(); + expect(mocks.reconcileVectorAgent).toHaveBeenCalledTimes(1); + expect(mocks.reconcileVectorAgent).toHaveBeenCalledWith("server-1"); + }); +}); diff --git a/apps/dokploy/__test__/logs-and-metrics/vector-config.test.ts b/apps/dokploy/__test__/logs-and-metrics/vector-config.test.ts new file mode 100644 index 000000000..98c4d70d3 --- /dev/null +++ b/apps/dokploy/__test__/logs-and-metrics/vector-config.test.ts @@ -0,0 +1,375 @@ +import { vrlString } from "@dokploy/server/services/logs-and-metrics/types"; +import { + buildMetricsScopeTransformSource, + buildVectorConfigYaml, + collectSecretValues, + redactSecrets, +} from "@dokploy/server/setup/vector-setup"; +import { describe, expect, it } from "vitest"; +import { parse } from "yaml"; + +const provider = (overrides: Record) => + ({ + apiKey: null, + apiSecret: null, + endpoint: null, + extraConfig: null, + signals: ["logs"], + ...overrides, + }) as any; + +const loki = provider({ + telemetryProviderId: "loki-1", + name: "loki", + providerType: "loki", + endpoint: "https://loki.example.com", +}); + +const prom = provider({ + telemetryProviderId: "prom-1", + name: "prom", + providerType: "prometheus_remote_write", + signals: ["metrics"], + endpoint: "http://prom.example.com:9090/api/v1/write", +}); + +const datadog = provider({ + telemetryProviderId: "dd-1", + name: "datadog", + providerType: "datadog", + signals: ["logs", "metrics"], + apiKey: "dd-key", +}); + +const build = ( + overrides: Partial[0]>, +) => + parse( + buildVectorConfigYaml({ + logProviders: [], + metricsProviders: [], + serverName: "my server", + ...overrides, + }), + ) as any; + +describe("buildVectorConfigYaml — logs", () => { + it("generates a source, a dokploy_scope transform and one sink per provider, and nothing for metrics", () => { + const config = build({ logProviders: [loki] }); + + 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(Object.keys(config.sinks)).toEqual(["sink_loki-1"]); + expect(config.sinks["sink_loki-1"].type).toBe("loki"); + expect(config.sinks["sink_loki-1"].inputs).toEqual(["dokploy_scope"]); + + expect(config.sources.host_metrics_source).toBeUndefined(); + expect(config.sources.cadvisor_source).toBeUndefined(); + expect(config.transforms.dokploy_metrics_scope).toBeUndefined(); + }); + + it("chains scope -> transform -> sink for betterstack/datadog, and scope -> sink direct for loki", () => { + const config = build({ + logProviders: [ + loki, + provider({ + telemetryProviderId: "bs-1", + name: "betterstack", + providerType: "betterstack", + endpoint: "https://in.logs.betterstack.com", + apiKey: "token", + }), + datadog, + ], + }); + + expect(config.sinks["sink_loki-1"].inputs).toEqual(["dokploy_scope"]); + 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"].inputs).toEqual([ + "dokploy_scope", + ]); + expect(config.sinks["sink_dd-1"].inputs).toEqual(["transform_dd-1"]); + }); + + it("tags every log with the server name and reads the Dokploy metadata straight off the container labels, with empty defaults for containers Dokploy did not deploy", () => { + const config = build({ logProviders: [loki], serverName: 'srv "quoted"' }); + + expect(config.transforms.dokploy_scope.source).toBe( + [ + '.dokploy_server = "srv \\"quoted\\""', + '.dokploy_organization = .label."dokploy.organization.id" || ""', + '.dokploy_project = .label."dokploy.project" || ""', + '.dokploy_project_id = .label."dokploy.project.id" || ""', + '.dokploy_environment = .label."dokploy.environment" || ""', + '.dokploy_environment_id = .label."dokploy.environment.id" || ""', + '.dokploy_application = .label."dokploy.application" || ""', + '.dokploy_application_id = .label."dokploy.application.id" || ""', + '.dokploy_service = .label."dokploy.service" || ""', + ].join("\n"), + ); + }); + + it("skips a provider whose config an adapter rejects at build time, instead of failing the whole org's config", () => { + const config = build({ + logProviders: [ + provider({ + telemetryProviderId: "broken-es", + name: "es-broken", + providerType: "elasticsearch", + endpoint: "https://es.example.com:9200", + extraConfig: { username: "elastic" }, + }), + loki, + ], + }); + + expect(Object.keys(config.sinks)).toEqual(["sink_loki-1"]); + }); + + it("only keeps containers of the owning organization on the local host, which is shared by every organization", () => { + const config = build({ logProviders: [loki], organizationId: "org-a" }); + + expect(config.transforms.dokploy_scope_local_only).toEqual({ + type: "filter", + inputs: ["dokploy_scope"], + condition: '.dokploy_organization == "org-a"', + }); + expect(config.sinks["sink_loki-1"].inputs).toEqual([ + "dokploy_scope_local_only", + ]); + }); + + it("does not add the organization filter for the per-server (non-local) path", () => { + const config = build({ logProviders: [loki] }); + + expect(config.transforms.dokploy_scope_local_only).toBeUndefined(); + }); +}); + +describe("buildVectorConfigYaml — metrics", () => { + it("with only metrics providers it emits the host and cAdvisor sources and no logs pipeline", () => { + const config = build({ metricsProviders: [prom] }); + + expect(config.sources.docker_logs_source).toBeUndefined(); + expect(config.transforms.dokploy_scope).toBeUndefined(); + expect(config.sources.host_metrics_source).toMatchObject({ + type: "host_metrics", + scrape_interval_secs: 30, + collectors: [ + "cpu", + "memory", + "disk", + "filesystem", + "network", + "load", + "host", + ], + }); + expect( + config.sources.host_metrics_source.filesystem.filesystems.excludes, + ).toEqual( + expect.arrayContaining([ + "overlay", + "tmpfs", + "squashfs", + "devtmpfs", + "nsfs", + "proc", + "sysfs", + "devpts", + "cgroup2", + ]), + ); + expect(config.sources.host_metrics_source.network.devices.excludes).toEqual( + ["veth*", "br-*", "docker*"], + ); + expect(config.sources.cadvisor_source).toEqual({ + type: "prometheus_scrape", + endpoints: ["http://127.0.0.1:4510/metrics"], + scrape_interval_secs: 30, + scrape_timeout_secs: 10, + }); + expect(config.transforms.dokploy_metrics_scope).toMatchObject({ + type: "remap", + inputs: ["host_metrics_source", "cadvisor_source"], + drop_on_abort: true, + }); + expect(Object.keys(config.sinks)).toEqual(["sink_prom-1_metrics"]); + expect(config.sinks["sink_prom-1_metrics"]).toMatchObject({ + type: "prometheus_remote_write", + inputs: ["dokploy_metrics_scope"], + }); + expect(config.transforms.dokploy_metrics_scope_local_only).toBeUndefined(); + }); + + it("on the local host it lets host series through and keeps only the owning organization's containers", () => { + const config = build({ metricsProviders: [prom], organizationId: "org-a" }); + + expect(config.transforms.dokploy_metrics_scope_local_only).toEqual({ + type: "filter", + inputs: ["dokploy_metrics_scope"], + condition: + '.namespace == "host" || .tags.dokploy_organization == "org-a"', + }); + expect(config.sinks["sink_prom-1_metrics"].inputs).toEqual([ + "dokploy_metrics_scope_local_only", + ]); + }); + + it("a provider assigned to both signals gets a logs sink and a metrics sink without clashing", () => { + const config = build({ + logProviders: [datadog], + metricsProviders: [datadog], + organizationId: "org-a", + }); + + expect(config.sinks["sink_dd-1"]).toMatchObject({ + type: "datadog_logs", + inputs: ["transform_dd-1"], + }); + expect(config.sinks["sink_dd-1_metrics"]).toMatchObject({ + type: "datadog_metrics", + inputs: ["dokploy_metrics_scope_local_only"], + }); + expect(config.transforms["transform_dd-1_metrics"]).toBeUndefined(); + }); + + it("tags every series with the server name, promotes non-empty dokploy labels and drops the cgroup id", () => { + const source = buildMetricsScopeTransformSource('srv "quoted"'); + + expect(source).toContain('.tags.dokploy_server = "srv \\"quoted\\""'); + expect(source).toContain( + 'if .namespace != "host" && !starts_with(string!(.name), "container_") { abort }', + ); + expect(source).toContain( + 'value = del(.tags.container_label_dokploy_application)\nif is_string(value) && value != "" { .tags.dokploy_application = value }', + ); + expect(source.trim().endsWith("del(.tags.id)")).toBe(true); + expect( + build({ metricsProviders: [prom] }).transforms.dokploy_metrics_scope + .source, + ).toBe(buildMetricsScopeTransformSource("my server")); + }); +}); + +describe("buildVectorConfigYaml — dollar signs", () => { + it("keeps every $ literal because the agent runs without env var interpolation", () => { + const yamlStr = buildVectorConfigYaml({ + logProviders: [ + provider({ + telemetryProviderId: "loki-1", + name: "loki", + providerType: "loki", + endpoint: "https://loki.example.com", + apiKey: "Pa$5w0rd$HOME", + extraConfig: { username: "12345" }, + }), + ], + metricsProviders: [prom], + serverName: "srv $ONE", + }); + + expect(yamlStr).not.toContain("$$"); + expect(yamlStr).toContain("srv $ONE"); + expect(parse(yamlStr).sinks["sink_loki-1"].auth.password).toBe( + "Pa$5w0rd$HOME", + ); + }); +}); + +describe("collectSecretValues / redactSecrets", () => { + it("collects endpoint/apiKey/apiSecret across all providers but only the password-type extraConfig fields", () => { + const providers = [ + { + providerType: "loki", + endpoint: "https://loki.example.com", + apiKey: "loki-key", + apiSecret: null, + extraConfig: { tenantId: "tenant-a", username: "12345" }, + }, + { + providerType: "datadog", + endpoint: null, + apiKey: "dd-key", + apiSecret: null, + extraConfig: { site: "datadoghq.eu" }, + }, + { + providerType: "aws_cloudwatch", + endpoint: null, + apiKey: "AKIA", + apiSecret: "shh", + extraConfig: { region: "us-east-1", metricsNamespace: "Dokploy" }, + }, + ] as any; + + const values = collectSecretValues(providers); + expect(values).toEqual( + expect.arrayContaining([ + "https://loki.example.com", + "loki-key", + "dd-key", + "AKIA", + "shh", + ]), + ); + for (const plain of [ + "tenant-a", + "12345", + "datadoghq.eu", + "us-east-1", + "Dokploy", + ]) { + expect(values).not.toContain(plain); + } + }); + + it("ignores null/empty credentials", () => { + const providers = [ + { + providerType: "elasticsearch", + 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 = [ + { + providerType: "loki", + endpoint: null, + apiKey: "hunter2-long-suffix", + apiSecret: "hunter2", + extraConfig: null, + }, + ] 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("vrlString", () => { + it("quotes and escapes like JSON but drops the control characters VRL rejects", () => { + expect(vrlString('my "server"')).toBe('"my \\"server\\""'); + expect(vrlString("back\\slash")).toBe('"back\\\\slash"'); + expect(vrlString("a\u0008b\u000cc\u0000d\u007fe\nf")).toBe('"abcdef"'); + }); +}); diff --git a/apps/dokploy/__test__/logs-and-metrics/vector-reconcile.test.ts b/apps/dokploy/__test__/logs-and-metrics/vector-reconcile.test.ts new file mode 100644 index 000000000..4d3775553 --- /dev/null +++ b/apps/dokploy/__test__/logs-and-metrics/vector-reconcile.test.ts @@ -0,0 +1,526 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { parse } from "yaml"; + +const mocks = vi.hoisted(() => ({ + findServerById: vi.fn(), + getWebServerSettings: vi.fn(), + findEnabledTelemetryProvidersByOrganization: vi.fn(), + execAsync: vi.fn(), + execAsyncRemote: vi.fn(), + inspectService: vi.fn(), + updateService: vi.fn(), + removeService: vi.fn(), + createService: vi.fn(), + getService: vi.fn(), + listContainers: vi.fn(), + killContainer: vi.fn(), +})); + +vi.mock("@dokploy/server/db", () => ({ db: {} })); +vi.mock("@dokploy/server/constants", () => ({ + IS_CLOUD: false, + paths: (isServer: boolean) => ({ + VECTOR_PATH: isServer ? "/etc/dokploy/vector" : "/local/dokploy/vector", + }), +})); +vi.mock("@dokploy/server/services/server", () => ({ + findServerById: mocks.findServerById, + findServersByOrganizationForVectorAgent: vi.fn(), + getAccessibleServerIds: vi.fn(), +})); +vi.mock("@dokploy/server/services/web-server-settings", () => ({ + getWebServerSettings: mocks.getWebServerSettings, +})); +vi.mock( + "@dokploy/server/services/logs-and-metrics/service", + async (importOriginal) => ({ + ...(await importOriginal< + typeof import("@dokploy/server/services/logs-and-metrics/service") + >()), + findEnabledTelemetryProvidersByOrganization: + mocks.findEnabledTelemetryProvidersByOrganization, + }), +); +vi.mock("@dokploy/server/utils/process/execAsync", () => ({ + execAsync: mocks.execAsync, + execAsyncRemote: mocks.execAsyncRemote, +})); +vi.mock("@dokploy/server/utils/servers/remote-docker", () => ({ + getRemoteDocker: async () => ({ + getService: mocks.getService, + createService: mocks.createService, + listContainers: mocks.listContainers, + getContainer: () => ({ kill: mocks.killContainer }), + }), +})); + +const { reconcileVectorAgent, removeVectorAgent } = await import( + "@dokploy/server/setup/vector-setup" +); + +const lokiProvider = { + telemetryProviderId: "lp-1", + name: "loki", + providerType: "loki", + signals: ["logs"], + endpoint: "https://loki.example.com", + apiKey: "sekret-token", + apiSecret: null, + extraConfig: null, + enabled: true, + organizationId: "org-a", +}; + +const promProvider = { + ...lokiProvider, + telemetryProviderId: "mp-1", + name: "prom", + providerType: "prometheus_remote_write", + signals: ["metrics"], + endpoint: "http://prom.example.com:9090/api/v1/write", + apiKey: null, +}; + +const notFound = () => + Object.assign(new Error("not found"), { statusCode: 404 }); + +const writtenYaml = (command: string) => { + const match = command.match(/echo "([^"]+)" \| base64 -d/); + return parse(Buffer.from(match?.[1] ?? "", "base64").toString("utf8")); +}; + +const commandsOf = (mock: typeof mocks.execAsync) => + mock.mock.calls.map((call) => String(call[call.length - 1])); + +beforeEach(() => { + for (const mock of Object.values(mocks)) mock.mockReset(); + mocks.getService.mockImplementation(() => ({ + inspect: mocks.inspectService, + update: mocks.updateService, + remove: mocks.removeService, + })); + mocks.inspectService.mockRejectedValue(notFound()); + mocks.removeService.mockRejectedValue(notFound()); + mocks.execAsync.mockResolvedValue({ stdout: "", stderr: "" }); + mocks.execAsyncRemote.mockResolvedValue({ stdout: "", stderr: "" }); + mocks.listContainers.mockResolvedValue([{ Id: "vector-container" }]); + mocks.killContainer.mockResolvedValue(undefined); +}); + +describe("reconcileVectorAgent", () => { + it("writes, validates and applies the YAML and deploys Vector for the saved selection", async () => { + mocks.getWebServerSettings.mockResolvedValue({ + vectorAgentOrganizationId: "org-a", + telemetryProviderIds: ["lp-1"], + }); + mocks.findEnabledTelemetryProvidersByOrganization.mockResolvedValue([ + lokiProvider, + ]); + + await reconcileVectorAgent(); + + const commands = commandsOf(mocks.execAsync); + expect(commands).toHaveLength(3); + expect(commands[0]).toContain( + "/local/dokploy/vector/vector.yaml.candidate.", + ); + expect(commands[1]).toMatch( + /timberio\/vector:\d+\.\d+\.\d+-alpine validate --skip-healthchecks/, + ); + expect(commands[2]).toMatch( + /^mv \/local\/dokploy\/vector\/vector\.yaml\.candidate\.\S+ \/local\/dokploy\/vector\/vector\.yaml$/, + ); + expect(mocks.execAsyncRemote).not.toHaveBeenCalled(); + + const config = writtenYaml(commands[0] ?? ""); + expect(config.sinks["sink_lp-1"].inputs).toEqual([ + "dokploy_scope_local_only", + ]); + expect(config.sources.host_metrics_source).toBeUndefined(); + + expect(mocks.createService).toHaveBeenCalledTimes(1); + expect(mocks.createService.mock.calls[0]?.[0]).toMatchObject({ + Name: "dokploy-vector", + }); + expect(mocks.getService).toHaveBeenCalledWith("dokploy-cadvisor"); + expect(mocks.removeService).toHaveBeenCalledTimes(1); + }); + + it("splits one selection into the logs and metrics pipelines by what each provider sends", async () => { + const datadogProvider = { + ...lokiProvider, + telemetryProviderId: "dd-1", + name: "datadog", + providerType: "datadog", + signals: ["logs", "metrics"], + endpoint: null, + apiKey: "dd-key", + }; + const unselected = { ...lokiProvider, telemetryProviderId: "lp-9" }; + mocks.getWebServerSettings.mockResolvedValue({ + vectorAgentOrganizationId: "org-a", + telemetryProviderIds: ["lp-1", "mp-1", "dd-1"], + }); + mocks.findEnabledTelemetryProvidersByOrganization.mockResolvedValue([ + lokiProvider, + promProvider, + datadogProvider, + unselected, + ]); + + await reconcileVectorAgent(); + + const config = writtenYaml(commandsOf(mocks.execAsync)[0] ?? ""); + const sinkIds = Object.keys(config.sinks); + expect(sinkIds.some((id) => id.includes("lp-1"))).toBe(true); + expect(sinkIds.some((id) => id.includes("mp-1"))).toBe(true); + expect(sinkIds.filter((id) => id.includes("dd-1")).length).toBe(2); + expect(sinkIds.some((id) => id.includes("lp-9"))).toBe(false); + expect(config.sources.docker_logs_source).toBeDefined(); + expect(config.sources.cadvisor_source).toBeDefined(); + expect(mocks.createService.mock.calls.map((c) => c[0].Name)).toEqual([ + "dokploy-vector", + "dokploy-cadvisor", + ]); + }); + + it("removes cAdvisor but keeps Vector when no selected provider sends metrics", async () => { + mocks.getWebServerSettings.mockResolvedValue({ + vectorAgentOrganizationId: "org-a", + telemetryProviderIds: ["lp-1", "mp-1"], + }); + mocks.findEnabledTelemetryProvidersByOrganization.mockResolvedValue([ + lokiProvider, + ]); + + await reconcileVectorAgent(); + + const config = writtenYaml(commandsOf(mocks.execAsync)[0] ?? ""); + expect(config.sources.cadvisor_source).toBeUndefined(); + expect(mocks.createService.mock.calls.map((c) => c[0].Name)).toEqual([ + "dokploy-vector", + ]); + expect(mocks.getService).toHaveBeenCalledWith("dokploy-cadvisor"); + expect(mocks.removeService).toHaveBeenCalledTimes(1); + }); + + it("filters by the organization only on the local host and runs on the server over SSH", async () => { + mocks.findServerById.mockResolvedValue({ + serverId: "server-1", + organizationId: "org-a", + name: "edge-1", + telemetryProviderIds: ["lp-1"], + }); + mocks.findEnabledTelemetryProvidersByOrganization.mockResolvedValue([ + lokiProvider, + ]); + + await reconcileVectorAgent("server-1"); + + expect(mocks.execAsync).not.toHaveBeenCalled(); + expect( + mocks.execAsyncRemote.mock.calls.every((c) => c[0] === "server-1"), + ).toBe(true); + const config = writtenYaml(commandsOf(mocks.execAsyncRemote)[0] ?? ""); + expect(config.transforms.dokploy_scope_local_only).toBeUndefined(); + expect(config.sinks["sink_lp-1"].inputs).toEqual(["dokploy_scope"]); + }); + + it("does not apply a config that vector validate rejects, redacts secrets and leaves the selection alone", async () => { + mocks.getWebServerSettings.mockResolvedValue({ + vectorAgentOrganizationId: "org-a", + telemetryProviderIds: ["lp-1"], + }); + mocks.findEnabledTelemetryProvidersByOrganization.mockResolvedValue([ + lokiProvider, + ]); + mocks.execAsync.mockImplementation(async (command: string) => { + if (command.includes("validate")) { + throw new Error("bad sink: password sekret-token rejected"); + } + return { stdout: "", stderr: "" }; + }); + + await expect(reconcileVectorAgent()).rejects.toThrow( + /failed validation, not applying it: bad sink: password \[redacted\] rejected/, + ); + + const commands = commandsOf(mocks.execAsync); + expect(commands.some((c) => c.startsWith("mv "))).toBe(false); + expect(commands.some((c) => c.startsWith("rm -f "))).toBe(true); + expect(mocks.createService).not.toHaveBeenCalled(); + }); + + it("removes the agent when no enabled provider is left in the selection", async () => { + mocks.getWebServerSettings.mockResolvedValue({ + vectorAgentOrganizationId: "org-a", + telemetryProviderIds: ["deleted-or-disabled"], + }); + mocks.findEnabledTelemetryProvidersByOrganization.mockResolvedValue([]); + + await reconcileVectorAgent(); + + expect(mocks.getService).toHaveBeenCalledWith("dokploy-cadvisor"); + expect(mocks.getService).toHaveBeenCalledWith("dokploy-vector"); + expect(mocks.removeService).toHaveBeenCalledTimes(2); + expect(commandsOf(mocks.execAsync)).toEqual([ + "rm -rf /local/dokploy/vector", + ]); + expect(mocks.createService).not.toHaveBeenCalled(); + }); + + it("removes the agent when the selection is empty, without an owner", async () => { + mocks.getWebServerSettings.mockResolvedValue({ + vectorAgentOrganizationId: null, + telemetryProviderIds: [], + }); + + await reconcileVectorAgent(); + + expect( + mocks.findEnabledTelemetryProvidersByOrganization, + ).not.toHaveBeenCalled(); + expect(mocks.removeService).toHaveBeenCalledTimes(2); + }); + + it("deploys cAdvisor next to Vector when metrics providers are assigned", async () => { + mocks.getWebServerSettings.mockResolvedValue({ + vectorAgentOrganizationId: "org-a", + telemetryProviderIds: ["mp-1"], + }); + mocks.findEnabledTelemetryProvidersByOrganization.mockResolvedValue([ + promProvider, + ]); + + await reconcileVectorAgent(); + + const config = writtenYaml(commandsOf(mocks.execAsync)[0] ?? ""); + expect(config.sources.docker_logs_source).toBeUndefined(); + expect(config.sources.cadvisor_source.endpoints).toEqual([ + "http://127.0.0.1:4510/metrics", + ]); + + expect(mocks.createService.mock.calls.map((c) => c[0].Name)).toEqual([ + "dokploy-vector", + "dokploy-cadvisor", + ]); + const cadvisor = mocks.createService.mock.calls[1]?.[0]; + expect(cadvisor.TaskTemplate.ContainerSpec.Image).toMatch( + /^ghcr\.io\/google\/cadvisor:v\d+\.\d+\.\d+$/, + ); + expect(cadvisor.TaskTemplate.ContainerSpec.Args).toEqual( + expect.arrayContaining([ + "--listen_ip=127.0.0.1", + "--port=4510", + "--docker_only=true", + "--store_container_labels=false", + ]), + ); + expect(cadvisor.TaskTemplate.ContainerSpec.HealthCheck).toEqual({ + Test: ["NONE"], + }); + expect(cadvisor.TaskTemplate.Networks).toEqual([{ Target: "host" }]); + expect(mocks.removeService).not.toHaveBeenCalled(); + }); + + it("removes the agent on a remote server when the selection is empty", async () => { + mocks.findServerById.mockResolvedValue({ + serverId: "server-1", + sshKeyId: "key-1", + organizationId: "org-a", + name: "edge-1", + telemetryProviderIds: [], + }); + mocks.findEnabledTelemetryProvidersByOrganization.mockResolvedValue([]); + + await reconcileVectorAgent("server-1"); + + expect( + mocks.findEnabledTelemetryProvidersByOrganization, + ).toHaveBeenCalled(); + expect(mocks.removeService).toHaveBeenCalledTimes(2); + expect(mocks.createService).not.toHaveBeenCalled(); + expect(commandsOf(mocks.execAsyncRemote)).toEqual([ + "rm -rf /etc/dokploy/vector", + ]); + }); + + it("redacts the config and secrets when writing or applying the candidate fails", async () => { + mocks.getWebServerSettings.mockResolvedValue({ + vectorAgentOrganizationId: "org-a", + telemetryProviderIds: ["lp-1"], + }); + mocks.findEnabledTelemetryProvidersByOrganization.mockResolvedValue([ + lokiProvider, + ]); + const echoCommand = async (command: string) => { + if (command.includes("base64 -d") || command.startsWith("mv ")) { + throw new Error(`Command failed: ${command} (token sekret-token)`); + } + return { stdout: "", stderr: "" }; + }; + mocks.execAsync.mockImplementation(echoCommand); + + const writeError = await reconcileVectorAgent().catch( + (e: Error) => e.message, + ); + expect(writeError).toMatch(/^Failed to write the Vector config: /); + expect(writeError).not.toContain("sekret-token"); + expect(writeError).not.toContain( + Buffer.from("data_dir").toString("base64").slice(0, 8), + ); + expect(writeError).toContain("[redacted]"); + + mocks.execAsync.mockImplementation(async (command: string) => { + if (command.startsWith("mv ")) return echoCommand(command); + return { stdout: "", stderr: "" }; + }); + const applyError = await reconcileVectorAgent().catch( + (e: Error) => e.message, + ); + expect(applyError).toMatch(/^Failed to apply the Vector config: /); + expect(applyError).not.toContain("sekret-token"); + expect(mocks.createService).not.toHaveBeenCalled(); + }); + + it("propagates a rejected service update instead of pretending the create succeeded", async () => { + mocks.getWebServerSettings.mockResolvedValue({ + vectorAgentOrganizationId: "org-a", + telemetryProviderIds: ["lp-1"], + }); + mocks.findEnabledTelemetryProvidersByOrganization.mockResolvedValue([ + lokiProvider, + ]); + mocks.inspectService.mockResolvedValue({ + Version: { Index: "7" }, + Spec: { TaskTemplate: { ForceUpdate: 2 } }, + }); + mocks.updateService.mockRejectedValue( + new Error("rpc error: invalid mount"), + ); + + await expect(reconcileVectorAgent()).rejects.toThrow("invalid mount"); + expect(mocks.createService).not.toHaveBeenCalled(); + }); + + it("propagates an inspect failure that is not a 404", async () => { + mocks.getWebServerSettings.mockResolvedValue({ + vectorAgentOrganizationId: "org-a", + telemetryProviderIds: ["lp-1"], + }); + mocks.findEnabledTelemetryProvidersByOrganization.mockResolvedValue([ + lokiProvider, + ]); + mocks.inspectService.mockRejectedValue( + Object.assign(new Error("daemon unreachable"), { statusCode: 500 }), + ); + + await expect(reconcileVectorAgent()).rejects.toThrow("daemon unreachable"); + expect(mocks.createService).not.toHaveBeenCalled(); + }); + + it("does not restart Vector on a config-only redeploy", async () => { + mocks.getWebServerSettings.mockResolvedValue({ + vectorAgentOrganizationId: "org-a", + telemetryProviderIds: ["lp-1"], + }); + mocks.findEnabledTelemetryProvidersByOrganization.mockResolvedValue([ + lokiProvider, + ]); + mocks.inspectService.mockImplementation(async () => ({ + Version: { Index: "7" }, + Spec: { + TaskTemplate: { + ForceUpdate: 2, + ContainerSpec: mocks.createService.mock.calls[0]?.[0] + ? undefined + : undefined, + }, + }, + })); + + await reconcileVectorAgent(); + expect(mocks.killContainer).toHaveBeenCalledWith({ signal: "SIGHUP" }); + expect(mocks.listContainers).toHaveBeenCalledWith({ + filters: JSON.stringify({ + label: ["com.docker.swarm.service.name=dokploy-vector"], + }), + }); + const firstSpec = mocks.updateService.mock.calls[0]?.[0]; + mocks.inspectService.mockResolvedValue({ + Version: { Index: "8" }, + Spec: { TaskTemplate: { ...firstSpec.TaskTemplate, ForceUpdate: 3 } }, + }); + mocks.updateService.mockClear(); + + await reconcileVectorAgent(); + expect(mocks.updateService).toHaveBeenCalledWith( + expect.objectContaining({ + version: 8, + TaskTemplate: expect.objectContaining({ ForceUpdate: 3 }), + }), + ); + }); + + it("updates an existing Vector service instead of creating it", async () => { + mocks.getWebServerSettings.mockResolvedValue({ + vectorAgentOrganizationId: "org-a", + telemetryProviderIds: ["lp-1"], + }); + mocks.findEnabledTelemetryProvidersByOrganization.mockResolvedValue([ + lokiProvider, + ]); + mocks.inspectService.mockResolvedValue({ + Version: { Index: "7" }, + Spec: { TaskTemplate: { ForceUpdate: 2 } }, + }); + + await reconcileVectorAgent(); + + expect(mocks.createService).not.toHaveBeenCalled(); + expect(mocks.updateService).toHaveBeenCalledWith( + expect.objectContaining({ + version: 7, + TaskTemplate: expect.objectContaining({ ForceUpdate: 3 }), + }), + ); + }); +}); + +describe("removeVectorAgent", () => { + it("removes cAdvisor, then Vector, then the config directory", async () => { + await removeVectorAgent(); + + expect(mocks.getService.mock.calls.map((c) => c[0])).toEqual([ + "dokploy-cadvisor", + "dokploy-vector", + ]); + expect(commandsOf(mocks.execAsync)).toEqual([ + "rm -rf /local/dokploy/vector", + ]); + }); + + it("keeps going when cAdvisor cannot be removed", async () => { + mocks.removeService + .mockRejectedValueOnce(new Error("docker hiccup")) + .mockRejectedValueOnce(notFound()); + + await removeVectorAgent(); + + expect(mocks.removeService).toHaveBeenCalledTimes(2); + expect(commandsOf(mocks.execAsync)).toEqual([ + "rm -rf /local/dokploy/vector", + ]); + }); + + it("refuses a server without an SSH key before touching cAdvisor or Vector", async () => { + mocks.findServerById.mockResolvedValue({ + serverId: "server-1", + sshKeyId: null, + }); + + await expect(removeVectorAgent("server-1")).rejects.toThrow(/SSH key/); + expect(mocks.getService).not.toHaveBeenCalled(); + expect(mocks.execAsyncRemote).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/dokploy/__test__/logs-and-metrics/vector-status.test.ts b/apps/dokploy/__test__/logs-and-metrics/vector-status.test.ts new file mode 100644 index 000000000..12131a7e7 --- /dev/null +++ b/apps/dokploy/__test__/logs-and-metrics/vector-status.test.ts @@ -0,0 +1,214 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + listTasks: vi.fn(), + inspectService: vi.fn(), + findServersByOrganizationForVectorAgent: vi.fn(), + getAccessibleServerIds: vi.fn(), + getWebServerSettings: vi.fn(), + findTelemetryProvidersByOrganization: vi.fn(), +})); + +vi.mock("@dokploy/server/db", () => ({ db: {} })); +vi.mock("@dokploy/server/constants", () => ({ + IS_CLOUD: false, + paths: () => ({ VECTOR_PATH: "/etc/dokploy/vector" }), +})); +vi.mock("@dokploy/server/services/server", () => ({ + findServerById: vi.fn(), + findServersByOrganizationForVectorAgent: + mocks.findServersByOrganizationForVectorAgent, + getAccessibleServerIds: mocks.getAccessibleServerIds, +})); +vi.mock("@dokploy/server/services/web-server-settings", () => ({ + getWebServerSettings: mocks.getWebServerSettings, +})); +vi.mock( + "@dokploy/server/services/logs-and-metrics/service", + async (importOriginal) => ({ + ...(await importOriginal< + typeof import("@dokploy/server/services/logs-and-metrics/service") + >()), + findTelemetryProvidersByOrganization: + mocks.findTelemetryProvidersByOrganization, + }), +); +vi.mock("@dokploy/server/utils/servers/remote-docker", () => ({ + getRemoteDocker: async () => ({ + listTasks: mocks.listTasks, + getService: () => ({ inspect: mocks.inspectService }), + }), +})); + +const { getVectorAgentTargets, withVectorTargetLock } = await import( + "@dokploy/server/setup/vector-setup" +); + +const session = { userId: "user-1", activeOrganizationId: "org-1" }; + +describe("getVectorAgentTargets", () => { + beforeEach(() => { + for (const mock of Object.values(mocks)) mock.mockReset(); + mocks.findServersByOrganizationForVectorAgent.mockResolvedValue([]); + mocks.inspectService.mockResolvedValue({}); + mocks.getAccessibleServerIds.mockResolvedValue(new Set()); + mocks.findTelemetryProvidersByOrganization.mockResolvedValue([ + { telemetryProviderId: "lp-1", enabled: true, signals: ["logs"] }, + { telemetryProviderId: "mp-1", enabled: true, signals: ["metrics"] }, + { telemetryProviderId: "mp-off", enabled: false, signals: ["metrics"] }, + ]); + mocks.getWebServerSettings.mockResolvedValue({ + vectorAgentOrganizationId: "org-1", + telemetryProviderIds: ["lp-1"], + }); + }); + + it("requires a running cAdvisor task when the target ships metrics", async () => { + mocks.getWebServerSettings.mockResolvedValue({ + vectorAgentOrganizationId: "org-1", + telemetryProviderIds: ["mp-1"], + }); + mocks.listTasks.mockImplementation( + async ({ filters }: { filters: string }) => + JSON.parse(filters).service[0] === "dokploy-vector" + ? [{ Status: { State: "running" } }] + : [], + ); + + const [local] = await getVectorAgentTargets(session); + expect(local?.status).toBe("not-running"); + expect(mocks.listTasks).toHaveBeenCalledTimes(2); + }); + + it("reports running when both Vector and cAdvisor have a running task", async () => { + mocks.getWebServerSettings.mockResolvedValue({ + vectorAgentOrganizationId: "org-1", + telemetryProviderIds: ["mp-1"], + }); + mocks.listTasks.mockResolvedValue([{ Status: { State: "running" } }]); + + const [local] = await getVectorAgentTargets(session); + expect(local?.status).toBe("running"); + }); + + it("does not look at cAdvisor for a logs-only target", async () => { + mocks.listTasks.mockResolvedValue([{ Status: { State: "running" } }]); + + await getVectorAgentTargets(session); + expect(mocks.listTasks).toHaveBeenCalledTimes(1); + }); + + it("does not look at cAdvisor when the only metrics provider is disabled", async () => { + mocks.getWebServerSettings.mockResolvedValue({ + vectorAgentOrganizationId: "org-1", + telemetryProviderIds: ["lp-1", "mp-off"], + }); + mocks.listTasks.mockResolvedValue([{ Status: { State: "running" } }]); + + const [local] = await getVectorAgentTargets(session); + expect(local?.status).toBe("running"); + expect(mocks.listTasks).toHaveBeenCalledTimes(1); + }); + + it("reports running only when a task of the service is running", async () => { + mocks.listTasks.mockResolvedValue([{ Status: { State: "running" } }]); + + const [local] = await getVectorAgentTargets(session); + expect(local?.status).toBe("running"); + expect(mocks.listTasks).toHaveBeenCalledWith({ + filters: JSON.stringify({ + service: ["dokploy-vector"], + "desired-state": ["running"], + }), + }); + }); + + it("reports not-running when the service has no running task", async () => { + mocks.listTasks.mockResolvedValue([{ Status: { State: "pending" } }]); + + const [local] = await getVectorAgentTargets(session); + expect(local?.status).toBe("not-running"); + }); + + it("reports stopped when the service does not exist", async () => { + mocks.inspectService.mockRejectedValue( + Object.assign(new Error("not found"), { statusCode: 404 }), + ); + mocks.listTasks.mockResolvedValue([]); + + const [local] = await getVectorAgentTargets(session); + expect(local?.status).toBe("stopped"); + expect(mocks.listTasks).not.toHaveBeenCalled(); + }); + + it("reports unknown on any other docker error", async () => { + mocks.inspectService.mockRejectedValue(new Error("connect ECONNREFUSED")); + + const [local] = await getVectorAgentTargets(session); + expect(local?.status).toBe("unknown"); + }); + + it("reports an empty selection for an unowned local host, even with ids left by a deleted organization", async () => { + mocks.getWebServerSettings.mockResolvedValue({ + vectorAgentOrganizationId: null, + telemetryProviderIds: ["lp-from-deleted-org", "mp-from-deleted-org"], + }); + mocks.listTasks.mockResolvedValue([]); + + const [local] = await getVectorAgentTargets(session); + expect(local).toMatchObject({ + serverId: null, + telemetryProviderIds: [], + }); + }); + + it("hides servers the member cannot access", async () => { + mocks.listTasks.mockResolvedValue([]); + mocks.findServersByOrganizationForVectorAgent.mockResolvedValue([ + { + serverId: "server-1", + name: "one", + ipAddress: "1.1.1.1", + telemetryProviderIds: [], + }, + { + serverId: "server-2", + name: "two", + ipAddress: "2.2.2.2", + telemetryProviderIds: [], + }, + ]); + mocks.getAccessibleServerIds.mockResolvedValue(new Set(["server-2"])); + + const targets = await getVectorAgentTargets(session); + expect(targets.map((t) => t.serverId)).toEqual([null, "server-2"]); + }); +}); + +describe("withVectorTargetLock", () => { + it("runs operations on the same target one after another", async () => { + const events: string[] = []; + let releaseFirst = () => {}; + const first = withVectorTargetLock("server-1", async () => { + events.push("first:start"); + await new Promise((resolve) => { + releaseFirst = resolve; + }); + events.push("first:end"); + throw new Error("first failed"); + }); + const second = withVectorTargetLock("server-1", async () => { + events.push("second"); + }); + const other = withVectorTargetLock("server-2", async () => { + events.push("other"); + }); + + await other; + expect(events).toEqual(["first:start", "other"]); + releaseFirst(); + await expect(first).rejects.toThrow("first failed"); + await second; + expect(events).toEqual(["first:start", "other", "first:end", "second"]); + }); +}); diff --git a/apps/dokploy/__test__/logs-and-metrics/vector-validate.test.ts b/apps/dokploy/__test__/logs-and-metrics/vector-validate.test.ts new file mode 100644 index 000000000..612b782d3 --- /dev/null +++ b/apps/dokploy/__test__/logs-and-metrics/vector-validate.test.ts @@ -0,0 +1,197 @@ +import { execFileSync } from "node:child_process"; +import { mkdtempSync, writeFileSync } from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { + buildVectorConfigYaml, + VECTOR_IMAGE, +} from "@dokploy/server/setup/vector-setup"; +import { describe, expect, it } from "vitest"; + +const dockerAvailable = (() => { + try { + execFileSync("docker", ["info"], { stdio: "ignore", timeout: 10_000 }); + return true; + } catch { + return false; + } +})(); + +const provider = (overrides: Record) => + ({ + apiKey: null, + apiSecret: null, + endpoint: null, + extraConfig: null, + enabled: true, + ...overrides, + }) as any; + +const logsOnly = [ + provider({ + telemetryProviderId: "loki1", + name: "loki", + providerType: "loki", + signals: ["logs"], + endpoint: "http://loki.example.com:3100", + apiKey: "Pa$5w0rd$HOME", + extraConfig: { username: "12345" }, + }), +]; + +const metricsOnly = [ + provider({ + telemetryProviderId: "prom1", + name: "prom", + providerType: "prometheus_remote_write", + signals: ["metrics"], + endpoint: "http://prom.example.com:9090/api/v1/write", + apiKey: "token", + extraConfig: { tenantId: "team-a" }, + }), + provider({ + telemetryProviderId: "nr1", + name: "new relic", + providerType: "new_relic", + signals: ["metrics"], + apiKey: "license", + extraConfig: { accountId: "123", region: "eu" }, + }), + provider({ + telemetryProviderId: "influx1", + name: "influx", + providerType: "influxdb", + signals: ["metrics"], + endpoint: "http://influx.example.com:8086", + apiKey: "token", + extraConfig: { org: "dokploy", bucket: "metrics" }, + }), +]; + +const both = [ + provider({ + telemetryProviderId: "dd1", + name: "datadog", + providerType: "datadog", + signals: ["logs", "metrics"], + apiKey: "dd-key", + }), + provider({ + telemetryProviderId: "cw1", + name: "cloudwatch", + providerType: "aws_cloudwatch", + signals: ["logs", "metrics"], + apiKey: "AKIAFAKE", + apiSecret: "secret", + extraConfig: { + region: "us-east-1", + logGroup: "/dokploy/logs", + metricsNamespace: "Dokploy", + }, + }), + provider({ + telemetryProviderId: "splunk1", + name: "splunk", + providerType: "splunk_hec", + signals: ["logs", "metrics"], + endpoint: "https://splunk.example.com:8088", + apiKey: "hec", + extraConfig: { index: "main", metricsIndex: "dokploy_metrics" }, + }), + provider({ + telemetryProviderId: "es1", + name: "elastic", + providerType: "elasticsearch", + signals: ["logs", "metrics"], + endpoint: "http://es.example.com:9200", + apiKey: "pw", + extraConfig: { username: "elastic" }, + }), + provider({ + telemetryProviderId: "bs1", + name: "better stack", + providerType: "betterstack", + signals: ["logs", "metrics"], + endpoint: "in.logs.betterstack.com", + apiKey: "token", + }), +]; + +const cases: Array<[string, Parameters[0]]> = [ + [ + "local logs only", + { + logProviders: logsOnly, + metricsProviders: [], + organizationId: "org_a", + serverName: "Dokploy Server (local)", + }, + ], + [ + "local metrics only", + { + logProviders: [], + metricsProviders: metricsOnly, + organizationId: "org_a", + serverName: "Dokploy Server (local)", + }, + ], + [ + "local every provider on both signals", + { + logProviders: [...logsOnly, ...both], + metricsProviders: [...metricsOnly, ...both], + organizationId: "org_a", + serverName: 'Dokploy "Server" $HOSTNAME (local)', + }, + ], + [ + "remote metrics only", + { logProviders: [], metricsProviders: metricsOnly, serverName: "edge-1" }, + ], + [ + "remote every provider on both signals", + { + logProviders: [...logsOnly, ...both], + metricsProviders: [...metricsOnly, ...both], + serverName: "edge-1", + }, + ], +]; + +describe.skipIf(!dockerAvailable)( + "vector validate on generated configs", + () => { + const dir = mkdtempSync(path.join(os.tmpdir(), "vector-validate-")); + + it.each(cases)( + "%s", + (name, input) => { + const file = path.join(dir, `${name.replace(/\W+/g, "-")}.yaml`); + writeFileSync(file, buildVectorConfigYaml(input)); + const output = execFileSync( + "docker", + [ + "run", + "--rm", + "-v", + "/var/run/docker.sock:/var/run/docker.sock:ro", + "-v", + `${file}:/etc/vector/vector.yaml:ro`, + VECTOR_IMAGE, + "validate", + "--skip-healthchecks", + "/etc/vector/vector.yaml", + ], + { + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + timeout: 120_000, + }, + ); + expect(output).toContain("Validated"); + }, + 150_000, + ); + }, +); diff --git a/apps/dokploy/__test__/logs-and-metrics/web-server-provider-ids.test.ts b/apps/dokploy/__test__/logs-and-metrics/web-server-provider-ids.test.ts new file mode 100644 index 000000000..eaf7a2cec --- /dev/null +++ b/apps/dokploy/__test__/logs-and-metrics/web-server-provider-ids.test.ts @@ -0,0 +1,86 @@ +import type { SQL } from "drizzle-orm"; +import { PgDialect } from "drizzle-orm/pg-core"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + findFirst: vi.fn(), + updates: [] as Array<{ values: Record; where: unknown }>, + returning: vi.fn(), +})); + +vi.mock("@dokploy/server/db", () => ({ + db: { + query: { webServerSettings: { findFirst: mocks.findFirst } }, + update: () => ({ + set: (values: Record) => ({ + where: (where: unknown) => { + mocks.updates.push({ values, where }); + return { returning: () => Promise.resolve(mocks.returning()) }; + }, + }), + }), + }, +})); + +const { releaseWebServerAgent, setWebServerProviderIds } = await import( + "@dokploy/server/services/web-server-settings" +); + +const render = (value: unknown) => new PgDialect().sqlToQuery(value as SQL).sql; + +beforeEach(() => { + mocks.updates = []; + mocks.findFirst.mockResolvedValue({ + id: "ws-1", + vectorAgentOrganizationId: "org-a", + }); + mocks.returning.mockReturnValue([{ id: "ws-1" }]); +}); + +describe("setWebServerProviderIds", () => { + it("claims the local agent for the organization when assigning providers", async () => { + const row = await setWebServerProviderIds("org-a", ["lp-1", "mp-1"]); + + expect(row).toEqual({ id: "ws-1" }); + const update = mocks.updates[0]; + expect(update?.values).toMatchObject({ + telemetryProviderIds: ["lp-1", "mp-1"], + vectorAgentOrganizationId: "org-a", + }); + expect(render(update?.where)).toBe( + '("webServerSettings"."id" = $1 and ("webServerSettings"."vectorAgentOrganizationId" is null or "webServerSettings"."vectorAgentOrganizationId" = $2))', + ); + }); + + it("releases the owner when the selection is cleared", async () => { + await setWebServerProviderIds("org-a", []); + + expect(mocks.updates[0]?.values).toMatchObject({ + telemetryProviderIds: [], + vectorAgentOrganizationId: null, + }); + }); + + it("returns null when another organization owns the local agent", async () => { + mocks.returning.mockReturnValue([]); + + await expect( + setWebServerProviderIds("org-b", ["lp-9"]), + ).resolves.toBeNull(); + }); +}); + +describe("releaseWebServerAgent", () => { + it("clears the selection and the owner only for the owning organization", async () => { + await releaseWebServerAgent("org-a"); + + const update = mocks.updates[0]; + expect(update?.values).toMatchObject({ + telemetryProviderIds: [], + vectorAgentOrganizationId: null, + }); + expect(render(update?.where)).toBe( + '"webServerSettings"."vectorAgentOrganizationId" = $1', + ); + }); +}); diff --git a/apps/dokploy/__test__/permissions/check-permission.test.ts b/apps/dokploy/__test__/permissions/check-permission.test.ts index ee0e61d7a..08594e6c9 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 telemetryProvider.read", async () => { + memberToReturn = mockMemberData("owner"); + await expect( + checkPermission(ctx, { telemetryProvider: ["read"] }), + ).resolves.toBeUndefined(); + }); + + it("admin bypasses telemetryProvider.create", async () => { + memberToReturn = mockMemberData("admin"); + await expect( + checkPermission(ctx, { telemetryProvider: ["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 telemetryProvider.read", async () => { + memberToReturn = mockMemberData("member"); + await expect( + checkPermission(ctx, { telemetryProvider: ["read"] }), + ).rejects.toThrow(); + }); + + it("member is denied telemetryProvider.create", async () => { + memberToReturn = mockMemberData("member"); + await expect( + checkPermission(ctx, { telemetryProvider: ["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..ecfbc380d 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", + "telemetryProvider", ]; 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..6f3f66618 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.telemetryProvider.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..2372df11f 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, + telemetryProviderIds: [], + vectorAgentOrganizationId: null, buildsConcurrency: 1, logCleanupCron: null, metricsConfig: { @@ -62,6 +64,10 @@ const baseSettings: WebServerSettings = { errorPageDescription: null, ogImageUrl: null, footerText: null, + logoLightUrl: null, + loginLogoLightUrl: null, + faviconLightUrl: null, + dualModeBranding: false, }, cleanupCacheApplications: false, cleanupCacheOnCompose: false, diff --git a/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx b/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx index d90ef92aa..4c9c3da41 100644 --- a/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx +++ b/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx @@ -554,7 +554,10 @@ export const RestoreBackup = ({ value={field.value} onValueChange={(value: DatabaseType) => { field.onChange(value); - form.setValue("metadata", {}); + form.setValue("metadata", { + serviceName: + form.getValues("metadata.serviceName") ?? "", + }); }} > @@ -581,7 +584,7 @@ export const RestoreBackup = ({
+ + + Restrict this provider to a single DNS zone: Dokploy + will only list and change records in it. Leave empty to + use every zone the credentials can access. When set, the{" "} + GET /domain/zone right is not needed. This + is enforced by Dokploy only, so also limit the OVH token + to that zone if you need a hard guarantee. + + + + )} + /> )} diff --git a/apps/dokploy/components/dashboard/settings/logs-and-metrics/handle-telemetry-provider.tsx b/apps/dokploy/components/dashboard/settings/logs-and-metrics/handle-telemetry-provider.tsx new file mode 100644 index 000000000..b40ac4c0a --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/logs-and-metrics/handle-telemetry-provider.tsx @@ -0,0 +1,575 @@ +import type { + TelemetryProviderType, + TelemetrySignal, +} from "@dokploy/server/services/logs-and-metrics/types"; +import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema"; +import { PenBoxIcon, PlusIcon } from "lucide-react"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; +import { telemetryProviderIcons } from "@/components/icons/telemetry-provider-icons"; +import { AlertBlock } from "@/components/shared/alert-block"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +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"]); + +const SIGNAL_OPTIONS: Array<{ + value: string; + label: string; + signals: TelemetrySignal[]; +}> = [ + { value: "logs", label: "Logs", signals: ["logs"] }, + { value: "metrics", label: "Metrics", signals: ["metrics"] }, + { + value: "logs,metrics", + label: "Logs & Metrics", + signals: ["logs", "metrics"], + }, +]; + +const providerForm = z.object({ + name: z.string().min(1, "Name is required"), + providerType: z.string().min(1, "Provider is required"), + signals: z.string().min(1, "Select what this provider sends"), + enabled: z.boolean(), + fields: z.record(z.string(), z.string().optional()), +}); + +type ProviderForm = z.infer; + +const toSignals = (value: string) => + SIGNAL_OPTIONS.find((option) => option.value === value)?.signals ?? []; + +interface Props { + telemetryProviderId?: string; +} + +export const agentsUsingProvider = ( + targets: + | Array<{ + name: string; + telemetryProviderIds: string[]; + }> + | undefined, + telemetryProviderId: string, +) => + (targets ?? []) + .filter((target) => + target.telemetryProviderIds.includes(telemetryProviderId), + ) + .map((target) => target.name) + .join(", "); + +export const HandleTelemetryProvider = ({ telemetryProviderId }: Props) => { + const utils = api.useUtils(); + const [isOpen, setIsOpen] = useState(false); + const [initialFields, setInitialFields] = useState>( + {}, + ); + const [clearedKeys, setClearedKeys] = useState>({}); + + const { data: availableTypes } = + api.telemetryProvider.availableTypes.useQuery(); + const { data: provider } = api.telemetryProvider.one.useQuery( + { telemetryProviderId: telemetryProviderId || "" }, + { enabled: !!telemetryProviderId, refetchOnWindowFocus: false }, + ); + + const createMutation = api.telemetryProvider.create.useMutation(); + const updateMutation = api.telemetryProvider.update.useMutation(); + const { + isPending: isSaving, + error, + isError, + } = telemetryProviderId ? updateMutation : createMutation; + const { + mutateAsync: testConnection, + isPending: isTestingRaw, + error: testError, + isError: testIsError, + } = api.telemetryProvider.testConnection.useMutation(); + const { + mutateAsync: testConnectionById, + isPending: isTestingById, + error: testByIdError, + isError: testByIdIsError, + } = api.telemetryProvider.testConnectionById.useMutation(); + + const form = useForm({ + defaultValues: { + name: "", + providerType: "", + signals: "", + enabled: true, + fields: {}, + }, + resolver: zodResolver(providerForm), + }); + + const providerType = form.watch("providerType"); + const signalsValue = form.watch("signals"); + const fieldValues = form.watch("fields"); + const signals = toSignals(signalsValue); + + const selectedType = availableTypes?.find((t) => t.type === providerType); + const signalOptions = SIGNAL_OPTIONS.filter((option) => + option.signals.every((s) => selectedType?.signals.includes(s)), + ); + const visibleFields = (selectedType?.credentialFields ?? []).filter( + (field) => !field.signal || signals.includes(field.signal), + ); + + useEffect(() => { + if (provider) { + const extraConfig = (provider.extraConfig ?? {}) as Record< + string, + string + >; + form.reset({ + name: provider.name, + providerType: provider.providerType, + signals: provider.signals.join(","), + enabled: provider.enabled, + fields: extraConfig, + }); + setInitialFields(extraConfig); + } else { + form.reset(); + setInitialFields({}); + } + setClearedKeys({}); + }, [form, provider, isOpen]); + + const buildPayload = () => { + const payload: { + name: string; + providerType: TelemetryProviderType; + signals: TelemetrySignal[]; + enabled: boolean; + endpoint?: string | null; + apiKey?: string | null; + apiSecret?: string | null; + extraConfig?: Record; + } = { + name: form.getValues("name"), + providerType: providerType as TelemetryProviderType, + signals, + enabled: form.getValues("enabled"), + }; + const extraConfig: Record = {}; + let hasExtraConfigFields = false; + for (const field of visibleFields) { + const value = fieldValues[field.key] ?? ""; + if ( + field.key === "endpoint" || + field.key === "apiKey" || + field.key === "apiSecret" + ) { + if (value) { + payload[field.key] = value; + } else if (clearedKeys[field.key]) { + payload[field.key] = null; + } + } else { + hasExtraConfigFields = true; + extraConfig[field.key] = value; + } + } + if (hasExtraConfigFields) { + payload.extraConfig = extraConfig; + } + return payload; + }; + + // Required fields are per type and signal, so zod can't know them up front. + const validateRequiredFields = () => { + let valid = true; + for (const field of visibleFields) { + const value = fieldValues[field.key] ?? ""; + const keepsStored = + !!telemetryProviderId && + TOP_LEVEL_KEYS.has(field.key) && + !clearedKeys[field.key]; + if (field.required && !value && !keepsStored) { + form.setError(`fields.${field.key}`, { + message: `${field.label} is required`, + }); + valid = false; + } + } + return valid; + }; + + const topLevelKeys = visibleFields + .filter((field) => TOP_LEVEL_KEYS.has(field.key)) + .map((field) => field.key); + const touchedTopLevelKeys = topLevelKeys.filter( + (key) => !!fieldValues[key] || clearedKeys[key], + ); + const touchedExtraConfigKeys = visibleFields + .filter((field) => !TOP_LEVEL_KEYS.has(field.key)) + .map((field) => field.key) + .filter((key) => (fieldValues[key] ?? "") !== (initialFields[key] ?? "")); + const touchedAnyCredentialField = + touchedTopLevelKeys.length > 0 || touchedExtraConfigKeys.length > 0; + const isPartiallyTouched = + !!telemetryProviderId && + touchedAnyCredentialField && + touchedTopLevelKeys.length < topLevelKeys.length; + const signalsChanged = + !!provider && signalsValue !== provider.signals.join(","); + + const showTestResult = (result: { warning?: string }) => { + if (result.warning) { + toast.message(result.warning); + } else { + toast.success("Connection tested successfully"); + } + }; + + const onTest = async () => { + if (!(await form.trigger(["providerType", "signals"]))) return; + if (telemetryProviderId && !touchedAnyCredentialField) { + await testConnectionById({ + telemetryProviderId, + ...(signalsChanged ? { signals } : {}), + }) + .then(showTestResult) + .catch((e) => { + toast.error( + e instanceof Error ? e.message : "Connection test failed", + ); + }); + return; + } + if (!validateRequiredFields()) return; + await testConnection(buildPayload()) + .then(showTestResult) + .catch((e) => { + toast.error(e instanceof Error ? e.message : "Connection test failed"); + }); + }; + + const onSubmit = async () => { + if (!validateRequiredFields()) return; + const payload = buildPayload(); + let warning: string | undefined; + let reapplied = false; + try { + if (telemetryProviderId) { + const result = await updateMutation.mutateAsync({ + telemetryProviderId, + ...payload, + }); + warning = result.warning; + reapplied = result.reapplied; + } else { + await createMutation.mutateAsync(payload); + } + } catch { + toast.error( + telemetryProviderId + ? "Error updating provider" + : "Error adding provider", + ); + return; + } + utils.telemetryProvider.all.invalidate(); + utils.telemetryProvider.serverStatus.invalidate(); + if (telemetryProviderId) { + utils.telemetryProvider.one.invalidate({ telemetryProviderId }); + } + const agents = + telemetryProviderId && + agentsUsingProvider( + utils.telemetryProvider.serverStatus.getData(), + telemetryProviderId, + ); + if (warning) { + toast.warning("Provider updated", { description: warning }); + } else { + toast.success( + telemetryProviderId ? "Provider updated" : "Provider added", + reapplied && agents + ? { description: `The Vector agent on ${agents} was re-applied.` } + : undefined, + ); + } + setIsOpen(false); + }; + + const isTesting = isTestingRaw || isTestingById; + + return ( + + + {telemetryProviderId ? ( + + ) : ( + + )} + + + + + {telemetryProviderId ? "Edit provider" : "Add a provider"} + + + Vector ships container logs and host/container metrics from every + server you deploy it on to this provider. + + + {(isError || testIsError || testByIdIsError) && ( + + {testError?.message || testByIdError?.message || error?.message} + + )} + +
+ + ( + + Name + + + + + + )} + /> + ( + + Provider + + + + )} + /> + + {selectedType && ( + ( + + Sends + + + + )} + /> + )} + + {visibleFields.map((credentialField) => ( + ( + + + {credentialField.label} + {!credentialField.required && " (Optional)"} + + {credentialField.helpText && ( + + {credentialField.helpText} + + )} +
+ + + + {telemetryProviderId && + TOP_LEVEL_KEYS.has(credentialField.key) && + !credentialField.required && ( + + )} +
+ +
+ )} + /> + ))} + + ( + + + + + Enabled + + )} + /> + + + + + {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/logs-and-metrics/show-servers.tsx b/apps/dokploy/components/dashboard/settings/logs-and-metrics/show-servers.tsx new file mode 100644 index 000000000..bc375a3d2 --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/logs-and-metrics/show-servers.tsx @@ -0,0 +1,361 @@ +import { Copy, Loader2, Server } from "lucide-react"; +import { useState } from "react"; +import { toast } from "sonner"; +import { telemetryProviderIcons } from "@/components/icons/telemetry-provider-icons"; +import { AlertBlock } from "@/components/shared/alert-block"; +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 { Checkbox } from "@/components/ui/checkbox"; +import { Label } from "@/components/ui/label"; +import { api } from "@/utils/api"; + +type Status = "running" | "not-running" | "stopped" | "unknown"; + +const statusBadge = (status: Status) => { + if (status === "running") { + return Running; + } + if (status === "not-running") { + return Not running; + } + if (status === "stopped") { + return Not deployed; + } + return Unreachable; +}; + +export const providerLabel = (provider: { name: string; signals: string[] }) => + `${provider.name} (${provider.signals.length === 2 ? "logs and metrics" : provider.signals[0]})`; + +export const ShowServers = () => { + const utils = api.useUtils(); + const [selection, setSelection] = useState>({}); + const [pendingTarget, setPendingTarget] = useState(null); + const [errors, setErrors] = useState< + Record + >({}); + + const { data: providers } = api.telemetryProvider.all.useQuery(); + const { data: targets, isPending } = + api.telemetryProvider.serverStatus.useQuery(undefined, { + refetchOnWindowFocus: false, + staleTime: 30_000, + trpc: { context: { skipBatch: true } }, + }); + + const { mutateAsync: deployOnServer } = + api.telemetryProvider.deployOnServer.useMutation(); + const { mutateAsync: removeOnServer } = + api.telemetryProvider.removeOnServer.useMutation(); + + const keyOf = (serverId: string | null) => serverId ?? "local"; + + // The selection can still hold providers deleted since it was made. + const idsFor = (serverId: string | null, saved: string[]) => { + const ids = selection[keyOf(serverId)] ?? saved; + return providers + ? ids.filter((id) => providers.some((p) => p.telemetryProviderId === id)) + : ids; + }; + + const toggleProvider = ( + serverId: string | null, + saved: string[], + telemetryProviderId: string, + ) => { + const key = keyOf(serverId); + const current = idsFor(serverId, saved); + setSelection({ + ...selection, + [key]: current.includes(telemetryProviderId) + ? current.filter((id) => id !== telemetryProviderId) + : [...current, telemetryProviderId], + }); + }; + + const setError = ( + serverId: string | null, + error?: { title: string; message: string }, + ) => + setErrors((prev) => { + const next = { ...prev }; + if (error) { + next[keyOf(serverId)] = error; + } else { + delete next[keyOf(serverId)]; + } + return next; + }); + + // The task is still starting right after a deploy, so look again once it had time to come up. + const refreshStatus = () => { + utils.telemetryProvider.serverStatus.invalidate(); + setTimeout(() => utils.telemetryProvider.serverStatus.invalidate(), 8_000); + }; + + const handleDeploy = async (serverId: string | null, ids: string[]) => { + setPendingTarget(keyOf(serverId)); + setError(serverId); + await deployOnServer({ serverId, providerIds: ids }) + .then(() => { + toast.success("Vector agent deployed"); + refreshStatus(); + }) + .catch((error) => { + setError(serverId, { + title: "Deploy failed", + message: + error instanceof Error + ? error.message + : "Failed to deploy the agent", + }); + refreshStatus(); + }) + .finally(() => setPendingTarget(null)); + }; + + const handleRemove = async (serverId: string | null) => { + setPendingTarget(keyOf(serverId)); + setError(serverId); + await removeOnServer({ serverId }) + .then(() => { + toast.success("Vector agent removed"); + setSelection((prev) => ({ ...prev, [keyOf(serverId)]: [] })); + refreshStatus(); + }) + .catch((error) => { + setError(serverId, { + title: "Remove failed", + message: + error instanceof Error + ? error.message + : "Failed to remove the agent", + }); + refreshStatus(); + }) + .finally(() => setPendingTarget(null)); + }; + + const hasProviders = (providers?.length ?? 0) > 0; + const visibleTargets = hasProviders + ? targets + : targets?.filter( + (target) => + target.telemetryProviderIds.length > 0 || + target.status === "running" || + target.status === "not-running", + ); + + return ( +
+ +
+ + + + Servers + + + Pick which providers each server ships to, then deploy its Vector + agent; a provider receives every signal it sends, and cAdvisor + runs next to the agent on 127.0.0.1:4510 when metrics are shipped. + Services deployed before Logs & Metrics need a redeploy to get the + labels it uses; until then their logs and container metrics are + not shipped from the Dokploy server. + + + + {isPending ? ( +
+ Checking servers... + +
+ ) : !hasProviders && !visibleTargets?.length ? ( +
+ + + Add a provider first — there's nowhere to ship to yet + +
+ ) : ( +
+ {visibleTargets?.map((target) => { + const key = keyOf(target.serverId); + const saved = target.telemetryProviderIds; + const ids = idsFor(target.serverId, saved); + const isBusy = pendingTarget === key; + const agentDeployed = target.status !== "stopped"; + const hasEnabledSelected = ids.some( + (id) => + providers?.find((p) => p.telemetryProviderId === id) + ?.enabled, + ); + + return ( +
+
+
+ + {target.name} + + {target.ipAddress && ( + + {target.ipAddress} + + )} +
+
+ {statusBadge(target.status as Status)} + {agentDeployed && ( + handleRemove(target.serverId)} + > + + + )} +
+
+ +
+ {hasProviders && ( + Providers + )} +
+ {providers?.map((provider) => { + const ProviderIcon = + telemetryProviderIcons[ + provider.providerType as keyof typeof telemetryProviderIcons + ]; + return ( +
+ + toggleProvider( + target.serverId, + saved, + provider.telemetryProviderId, + ) + } + /> + {ProviderIcon && ( + + )} + + {!provider.enabled && ( + Disabled + )} +
+ ); + })} +
+
+ + {errors[key] && ( + +
+ + {errors[key].title} + +
+															{errors[key].message}
+														
+ +
+
+ )} + + {hasProviders && ( +
+ {ids.length > 0 && !hasEnabledSelected && ( + + Every selected provider is disabled — nothing + would ship. + + )} + +
+ )} +
+ ); + })} + + {visibleTargets?.length === 0 && ( +
+ + + You don't have any server yet + +
+ )} +
+ )} +
+
+
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/settings/logs-and-metrics/show-telemetry-providers.tsx b/apps/dokploy/components/dashboard/settings/logs-and-metrics/show-telemetry-providers.tsx new file mode 100644 index 000000000..afe48a79f --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/logs-and-metrics/show-telemetry-providers.tsx @@ -0,0 +1,196 @@ +import { Loader2, ScrollText, Trash2 } from "lucide-react"; +import { toast } from "sonner"; +import { telemetryProviderIcons } from "@/components/icons/telemetry-provider-icons"; +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 { HandleTelemetryProvider } from "./handle-telemetry-provider"; + +interface TelemetryProviderRowProps { + provider: { + telemetryProviderId: string; + name: string; + enabled: boolean; + providerType: string; + signals: string[]; + }; + index: number; + typeLabel: string; + canEdit: boolean; + canDelete: boolean; + onDeleted: () => void; +} + +const TelemetryProviderRow = ({ + provider, + index, + typeLabel, + canEdit, + canDelete, + onDeleted, +}: TelemetryProviderRowProps) => { + const { mutateAsync, isPending: isRemoving } = + api.telemetryProvider.remove.useMutation(); + const ProviderIcon = + telemetryProviderIcons[ + provider.providerType as keyof typeof telemetryProviderIcons + ]; + + return ( +
+
+
+ {ProviderIcon && } +
+ + {index + 1}. {provider.name} + + {provider.enabled ? "Enabled" : "Disabled"} + + {provider.signals.includes("logs") && ( + Logs + )} + {provider.signals.includes("metrics") && ( + Metrics + )} + +
{typeLabel}
+
+
+ +
+ {canEdit && ( + + )} + + {canDelete && ( + { + await mutateAsync({ + telemetryProviderId: provider.telemetryProviderId, + }) + .then((result) => { + if (result.warning) { + toast.warning("Provider deleted", { + description: result.warning, + }); + } else { + toast.success( + "Provider deleted — the agents that used it were re-applied", + ); + } + onDeleted(); + }) + .catch(() => { + toast.error("Error deleting provider"); + }); + }} + > + + + )} +
+
+
+ ); +}; + +export const ShowTelemetryProviders = () => { + const utils = api.useUtils(); + const { data, isPending, refetch } = api.telemetryProvider.all.useQuery(); + const { data: availableTypes } = + api.telemetryProvider.availableTypes.useQuery(); + const { data: permissions } = api.user.getPermissions.useQuery(); + const providerListLabel = availableTypes?.length + ? ` (${availableTypes.map((t) => t.label).join(", ")})` + : ""; + + return ( +
+ +
+ + + + Logs & Metrics + + + {`Ship container logs and host/container metrics to an external provider${providerListLabel}. Add one here, choose what it sends, then deploy the Vector agent on the servers below.`} + + + + {isPending ? ( +
+ Loading... + +
+ ) : ( + <> + {data?.length === 0 ? ( +
+ + + You don't have any provider configured + + {permissions?.telemetryProvider.create && ( + + )} +
+ ) : ( +
+
+ {data?.map((provider, index) => ( + t.type === provider.providerType, + )?.label ?? provider.providerType + } + canEdit={!!permissions?.telemetryProvider.create} + canDelete={!!permissions?.telemetryProvider.delete} + onDeleted={() => { + refetch(); + utils.telemetryProvider.serverStatus.invalidate(); + }} + /> + ))} +
+ + {permissions?.telemetryProvider.create && ( +
+ +
+ )} +
+ )} + + )} +
+
+
+
+ ); +}; 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 d5456b8c7..eb5b1b711 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 reach this host to remove the Vector logs and metrics agent; if it was deployed it may still be running: ${result.vectorAgentRemovalWarning}`, + ); + } setOpen(false); utils.server.all.invalidate(); } catch (error: any) { diff --git a/apps/dokploy/components/icons/telemetry-provider-icons.tsx b/apps/dokploy/components/icons/telemetry-provider-icons.tsx new file mode 100644 index 000000000..e8cfe5cd5 --- /dev/null +++ b/apps/dokploy/components/icons/telemetry-provider-icons.tsx @@ -0,0 +1,105 @@ +import { AwsIcon } from "@/components/icons/vault-provider-icons"; + +interface Props { + className?: string; +} + +export const GrafanaLokiIcon = ({ className }: Props) => ( + + + +); + +export const DatadogIcon = ({ className }: Props) => ( + + + +); + +export const BetterStackIcon = ({ className }: Props) => ( + + + +); + +export const ElasticsearchIcon = ({ className }: Props) => ( + + + +); + +export const SplunkIcon = ({ className }: Props) => ( + + + +); + +export const PrometheusIcon = ({ className }: Props) => ( + + + +); + +export const NewRelicIcon = ({ className }: Props) => ( + + + +); + +export const InfluxDbIcon = ({ className }: Props) => ( + + + +); + +export const telemetryProviderIcons = { + loki: GrafanaLokiIcon, + prometheus_remote_write: PrometheusIcon, + new_relic: NewRelicIcon, + influxdb: InfluxDbIcon, + datadog: DatadogIcon, + aws_cloudwatch: AwsIcon, + splunk_hec: SplunkIcon, + elasticsearch: ElasticsearchIcon, + betterstack: BetterStackIcon, +} as const; diff --git a/apps/dokploy/components/layouts/onboarding-layout.tsx b/apps/dokploy/components/layouts/onboarding-layout.tsx index ed46c1930..3776c2db5 100644 --- a/apps/dokploy/components/layouts/onboarding-layout.tsx +++ b/apps/dokploy/components/layouts/onboarding-layout.tsx @@ -19,6 +19,10 @@ export const OnboardingLayout = ({ children, leftPanel }: Props) => { "\u201CThe Open Source alternative to Netlify, Vercel, Heroku.\u201D"; const logoUrl = whitelabeling?.loginLogoUrl || whitelabeling?.logoUrl || undefined; + const logoLightUrl = + whitelabeling?.loginLogoLightUrl || + whitelabeling?.logoLightUrl || + undefined; return (
@@ -28,7 +32,11 @@ export const OnboardingLayout = ({ children, leftPanel }: Props) => { href="/" className="relative z-20 flex items-center text-lg font-medium gap-4 text-primary" > - + {appName}
diff --git a/apps/dokploy/components/layouts/side.tsx b/apps/dokploy/components/layouts/side.tsx index 1e20a0fbe..59e628aa0 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: "Logs & Metrics", + url: "/dashboard/settings/logs-and-metrics", + icon: ScrollText, + isEnabled: ({ permissions }) => !!permissions?.telemetryProvider.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..ce36dbae8 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", + telemetryProvider: "Logs & Metrics", }; 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 797bc8d20..e7d7ae316 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", }, + telemetryProvider: { + label: "Logs & Metrics", + description: + "Manage the providers (Grafana Loki, Prometheus, Datadog, New Relic, etc.) that container logs and metrics are shipped to", + }, }; /** Descriptions for each action within a resource */ @@ -475,6 +480,21 @@ const ACTION_META: Record< description: "Remove DNS providers and delete their records", }, }, + telemetryProvider: { + read: { + label: "Read", + description: "View configured logs and metrics providers", + }, + create: { + label: "Create", + description: + "Add new providers, test their connection and deploy the Vector agent", + }, + delete: { + label: "Delete", + description: "Remove logs and metrics providers", + }, + }, }; /** Resources that should be hidden from the custom role editor (better-auth internals) */ diff --git a/apps/dokploy/components/proprietary/whitelabeling/whitelabeling-preview.tsx b/apps/dokploy/components/proprietary/whitelabeling/whitelabeling-preview.tsx index d53af90a2..5d1beea37 100644 --- a/apps/dokploy/components/proprietary/whitelabeling/whitelabeling-preview.tsx +++ b/apps/dokploy/components/proprietary/whitelabeling/whitelabeling-preview.tsx @@ -1,5 +1,6 @@ "use client"; +import { useTheme } from "next-themes"; import { Card, CardContent, @@ -12,12 +13,19 @@ interface WhitelabelingPreviewProps { config: { appName?: string; logoUrl?: string; + logoLightUrl?: string; + dualModeBranding?: boolean; footerText?: string; }; } export function WhitelabelingPreview({ config }: WhitelabelingPreviewProps) { const appName = config.appName || "Dokploy"; + const { resolvedTheme } = useTheme(); + const effectiveLogoUrl = + config.dualModeBranding && resolvedTheme === "light" && config.logoLightUrl + ? config.logoLightUrl + : config.logoUrl; return ( @@ -31,9 +39,9 @@ export function WhitelabelingPreview({ config }: WhitelabelingPreviewProps) {
{/* Simulated sidebar header */}
- {config.logoUrl ? ( + {effectiveLogoUrl ? ( Preview Logo diff --git a/apps/dokploy/components/proprietary/whitelabeling/whitelabeling-settings.tsx b/apps/dokploy/components/proprietary/whitelabeling/whitelabeling-settings.tsx index 3ba9e80bd..ee0e54e78 100644 --- a/apps/dokploy/components/proprietary/whitelabeling/whitelabeling-settings.tsx +++ b/apps/dokploy/components/proprietary/whitelabeling/whitelabeling-settings.tsx @@ -26,6 +26,7 @@ import { FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; +import { Switch } from "@/components/ui/switch"; import { Textarea } from "@/components/ui/textarea"; import { api } from "@/utils/api"; import { WhitelabelingPreview } from "./whitelabeling-preview"; @@ -36,20 +37,42 @@ const safeUrlField = z message: "Only http:// and https:// URLs are allowed", }); -const formSchema = z.object({ - appName: z.string(), - appDescription: z.string(), - logoUrl: safeUrlField, - faviconUrl: safeUrlField, - customCss: z.string(), - loginLogoUrl: safeUrlField, - supportUrl: safeUrlField, - docsUrl: safeUrlField, - errorPageTitle: z.string(), - errorPageDescription: z.string(), - footerText: z.string(), - ogImageUrl: safeUrlField, -}); +const formSchema = z + .object({ + appName: z.string(), + appDescription: z.string(), + logoUrl: safeUrlField, + faviconUrl: safeUrlField, + customCss: z.string(), + loginLogoUrl: safeUrlField, + supportUrl: safeUrlField, + docsUrl: safeUrlField, + errorPageTitle: z.string(), + errorPageDescription: z.string(), + footerText: z.string(), + ogImageUrl: safeUrlField, + logoLightUrl: z.string(), + loginLogoLightUrl: z.string(), + faviconLightUrl: z.string(), + dualModeBranding: z.boolean(), + }) + .superRefine((data, ctx) => { + if (!data.dualModeBranding) return; + for (const field of [ + "logoLightUrl", + "loginLogoLightUrl", + "faviconLightUrl", + ] as const) { + const val = data[field]; + if (val !== "" && !/^https?:\/\//i.test(val)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Only http:// and https:// URLs are allowed", + path: [field], + }); + } + } + }); type FormSchema = z.infer; @@ -195,6 +218,10 @@ export function WhitelabelingSettings() { errorPageDescription: "", footerText: "", ogImageUrl: "", + logoLightUrl: "", + loginLogoLightUrl: "", + faviconLightUrl: "", + dualModeBranding: false, }, resolver: zodResolver(formSchema), }); @@ -214,6 +241,10 @@ export function WhitelabelingSettings() { errorPageDescription: data.errorPageDescription ?? "", footerText: data.footerText ?? "", ogImageUrl: data.ogImageUrl ?? "", + logoLightUrl: data.logoLightUrl ?? "", + loginLogoLightUrl: data.loginLogoLightUrl ?? "", + faviconLightUrl: data.faviconLightUrl ?? "", + dualModeBranding: data.dualModeBranding ?? false, }); } }, [data, form]); @@ -244,6 +275,10 @@ export function WhitelabelingSettings() { errorPageDescription: values.errorPageDescription || null, footerText: values.footerText || null, ogImageUrl: values.ogImageUrl || null, + logoLightUrl: values.logoLightUrl || null, + loginLogoLightUrl: values.loginLogoLightUrl || null, + faviconLightUrl: values.faviconLightUrl || null, + dualModeBranding: values.dualModeBranding, }, }) .then(async () => { @@ -412,6 +447,111 @@ export function WhitelabelingSettings() { + {/* Light Mode Logos Section */} + + +
+
+ Light Mode Logos + + Use separate logos for light and dark themes. When enabled, + the logos in Branding are used for dark mode. + +
+ ( + + + { + field.onChange(checked); + if (!checked) { + form.clearErrors([ + "logoLightUrl", + "loginLogoLightUrl", + "faviconLightUrl", + ]); + } + }} + /> + + + )} + /> +
+
+
+
+ + ( + + Logo URL + + + + + + )} + /> + + ( + + Login Page Logo URL + + + + + + )} + /> + + ( + + Favicon URL + + + + + Follows the browser/OS theme, not the website theme. + + + + )} + /> + +
+
+
+ {/* Appearance Section */} diff --git a/apps/dokploy/components/shared/logo.tsx b/apps/dokploy/components/shared/logo.tsx index 2c316e22f..500925df3 100644 --- a/apps/dokploy/components/shared/logo.tsx +++ b/apps/dokploy/components/shared/logo.tsx @@ -1,16 +1,25 @@ +import { useTheme } from "next-themes"; import { cn } from "@/lib/utils"; interface Props { className?: string; logoUrl?: string; + logoLightUrl?: string; } -export const Logo = ({ className = "size-14", logoUrl }: Props) => { - if (logoUrl) { +export const Logo = ({ + className = "size-14", + logoUrl, + logoLightUrl, +}: Props) => { + const { resolvedTheme } = useTheme(); + const effectiveUrl = + resolvedTheme === "light" && logoLightUrl ? logoLightUrl : logoUrl; + + if (effectiveUrl) { return ( - // biome-ignore lint/performance/noImgElement: this is for dynamic logo loading Organization Logo diff --git a/apps/dokploy/drizzle/0197_logs_and_metrics.sql b/apps/dokploy/drizzle/0197_logs_and_metrics.sql new file mode 100644 index 000000000..8bd5077d5 --- /dev/null +++ b/apps/dokploy/drizzle/0197_logs_and_metrics.sql @@ -0,0 +1,20 @@ +CREATE TYPE "public"."TelemetryProviderType" AS ENUM('loki', 'datadog', 'betterstack', 'elasticsearch', 'splunk_hec', 'aws_cloudwatch', 'prometheus_remote_write', 'new_relic', 'influxdb');--> statement-breakpoint +CREATE TABLE "telemetryProvider" ( + "telemetryProviderId" text PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "providerType" "TelemetryProviderType" NOT NULL, + "signals" text[] 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 "telemetryProviderIds" text[] DEFAULT ARRAY[]::text[] NOT NULL;--> statement-breakpoint +ALTER TABLE "webServerSettings" ADD COLUMN "telemetryProviderIds" text[] DEFAULT ARRAY[]::text[] NOT NULL;--> statement-breakpoint +ALTER TABLE "webServerSettings" ADD COLUMN "vectorAgentOrganizationId" text;--> statement-breakpoint +ALTER TABLE "telemetryProvider" ADD CONSTRAINT "telemetryProvider_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_vectorAgentOrganizationId_organization_id_fk" FOREIGN KEY ("vectorAgentOrganizationId") 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..ad06a0402 --- /dev/null +++ b/apps/dokploy/drizzle/meta/0197_snapshot.json @@ -0,0 +1,9314 @@ +{ + "id": "e2f2cd03-52dd-457a-8854-1ef6d0b5d168", + "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.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 + }, + "telemetryProviderIds": { + "name": "telemetryProviderIds", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "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.telemetryProvider": { + "name": "telemetryProvider", + "schema": "", + "columns": { + "telemetryProviderId": { + "name": "telemetryProviderId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerType": { + "name": "providerType", + "type": "TelemetryProviderType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "signals": { + "name": "signals", + "type": "text[]", + "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": { + "telemetryProvider_organizationId_organization_id_fk": { + "name": "telemetryProvider_organizationId_organization_id_fk", + "tableFrom": "telemetryProvider", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "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 * * *'" + }, + "telemetryProviderIds": { + "name": "telemetryProviderIds", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "vectorAgentOrganizationId": { + "name": "vectorAgentOrganizationId", + "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_vectorAgentOrganizationId_organization_id_fk": { + "name": "webServerSettings_vectorAgentOrganizationId_organization_id_fk", + "tableFrom": "webServerSettings", + "tableTo": "organization", + "columnsFrom": [ + "vectorAgentOrganizationId" + ], + "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.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.TelemetryProviderType": { + "name": "TelemetryProviderType", + "schema": "public", + "values": [ + "loki", + "datadog", + "betterstack", + "elasticsearch", + "splunk_hec", + "aws_cloudwatch", + "prometheus_remote_write", + "new_relic", + "influxdb" + ] + }, + "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..dfefaa435 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": 1791456930312, + "tag": "0197_logs_and_metrics", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index aed087cac..fe40a69b8 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -48,6 +48,8 @@ "@ai-sdk/mistral": "^3.0.20", "@ai-sdk/openai": "^3.0.29", "@ai-sdk/openai-compatible": "^2.0.30", + "@aws-sdk/client-cloudwatch": "^3.1108.0", + "@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/_document.tsx b/apps/dokploy/pages/_document.tsx index a8f3b5931..5e6b6948b 100644 --- a/apps/dokploy/pages/_document.tsx +++ b/apps/dokploy/pages/_document.tsx @@ -13,6 +13,7 @@ interface WhitelabelingDocumentProps { appDescription: string | null; ogImageUrl: string | null; faviconHref: string | null; + faviconLightHref: string | null; customCss: string | null; baseUrl: string; } @@ -22,6 +23,7 @@ export default function Document({ appDescription, ogImageUrl, faviconHref, + faviconLightHref, customCss, baseUrl, }: WhitelabelingDocumentProps) { @@ -44,7 +46,22 @@ export default function Document({ - + {faviconLightHref ? ( + <> + + + + ) : ( + + )} {customCss && (