diff --git a/apps/dokploy/__test__/databases/database-networks.test.ts b/apps/dokploy/__test__/databases/database-networks.test.ts new file mode 100644 index 000000000..1278ec5ec --- /dev/null +++ b/apps/dokploy/__test__/databases/database-networks.test.ts @@ -0,0 +1,259 @@ +import { buildLibsql } from "@dokploy/server/utils/databases/libsql"; +import { buildMariadb } from "@dokploy/server/utils/databases/mariadb"; +import { buildMongo } from "@dokploy/server/utils/databases/mongo"; +import { buildMysql } from "@dokploy/server/utils/databases/mysql"; +import { buildPostgres } from "@dokploy/server/utils/databases/postgres"; +import { buildRedis } from "@dokploy/server/utils/databases/redis"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const { + inspectMock, + updateMock, + getServiceMock, + createServiceMock, + getRemoteDockerMock, + resolveServiceNetworksMock, +} = vi.hoisted(() => { + const inspect = vi.fn(); + const update = vi.fn(); + const getService = vi.fn(() => ({ inspect, update })); + const createService = vi.fn(async () => undefined); + const getRemoteDocker = vi.fn(async () => ({ + getService, + createService, + })); + const resolveServiceNetworks = vi.fn(async () => [ + { Target: "dokploy-network" }, + { Target: "custom-overlay" }, + ]); + return { + inspectMock: inspect, + updateMock: update, + getServiceMock: getService, + createServiceMock: createService, + getRemoteDockerMock: getRemoteDocker, + resolveServiceNetworksMock: resolveServiceNetworks, + }; +}); + +vi.mock("@dokploy/server/utils/servers/remote-docker", () => ({ + getRemoteDocker: getRemoteDockerMock, +})); + +vi.mock("@dokploy/server/services/network", () => ({ + resolveServiceNetworks: resolveServiceNetworksMock, +})); + +vi.mock("@dokploy/server/utils/vault", () => ({ + withResolvedVaultRefs: vi.fn(async (arg) => arg), +})); + +const createBaseDb = (appName: string) => ({ + appName, + databaseName: "testdb", + databaseUser: "testuser", + databasePassword: "testpassword", + dockerImage: "postgres:16", + externalPort: 5432, + memoryLimit: null, + memoryReservation: null, + cpuLimit: null, + cpuReservation: null, + command: null, + args: null, + mounts: [], + environment: { + project: { env: null }, + env: null, + }, + serverId: null, + networkIds: ["custom-overlay-id"], + detachDokployNetwork: false, +}); + +describe("database networks configuration", () => { + beforeEach(() => { + inspectMock.mockReset(); + updateMock.mockReset(); + getServiceMock.mockClear(); + createServiceMock.mockClear(); + getRemoteDockerMock.mockClear(); + getRemoteDockerMock.mockResolvedValue({ + getService: getServiceMock, + createService: createServiceMock, + }); + getServiceMock.mockReturnValue({ + inspect: inspectMock, + update: updateMock, + }); + }); + + describe("buildPostgres", () => { + it("attaches networks at both root and TaskTemplate when creating service", async () => { + inspectMock.mockRejectedValue(new Error("service not found")); + const db = createBaseDb("postgres-test-1"); + + await buildPostgres(db as any); + + expect(createServiceMock).toHaveBeenCalledTimes(1); + const [settings] = createServiceMock.mock.calls[0] as [any]; + expect(settings.Networks).toEqual([ + { Target: "dokploy-network" }, + { Target: "custom-overlay" }, + ]); + expect(settings.TaskTemplate.Networks).toEqual([ + { Target: "dokploy-network" }, + { Target: "custom-overlay" }, + ]); + }); + + it("preserves root and TaskTemplate networks when updating service", async () => { + inspectMock.mockResolvedValue({ + Version: { Index: "42" }, + Spec: { + TaskTemplate: { ForceUpdate: 1 }, + }, + }); + const db = createBaseDb("postgres-test-2"); + + await buildPostgres(db as any); + + expect(updateMock).toHaveBeenCalledTimes(1); + const [updatePayload] = updateMock.mock.calls[0] as [any]; + expect(updatePayload.Networks).toEqual([ + { Target: "dokploy-network" }, + { Target: "custom-overlay" }, + ]); + expect(updatePayload.TaskTemplate.Networks).toEqual([ + { Target: "dokploy-network" }, + { Target: "custom-overlay" }, + ]); + expect(updatePayload.TaskTemplate.ForceUpdate).toBe(2); + }); + }); + + describe("buildMysql", () => { + it("attaches networks at both root and TaskTemplate when creating and updating service", async () => { + inspectMock.mockResolvedValue({ + Version: { Index: "10" }, + Spec: { + TaskTemplate: { ForceUpdate: 0 }, + }, + }); + const db = createBaseDb("mysql-test-1"); + + await buildMysql(db as any); + + expect(updateMock).toHaveBeenCalledTimes(1); + const [updatePayload] = updateMock.mock.calls[0] as [any]; + expect(updatePayload.Networks).toEqual([ + { Target: "dokploy-network" }, + { Target: "custom-overlay" }, + ]); + expect(updatePayload.TaskTemplate.Networks).toEqual([ + { Target: "dokploy-network" }, + { Target: "custom-overlay" }, + ]); + }); + }); + + describe("buildMariadb", () => { + it("attaches networks at both root and TaskTemplate when creating and updating service", async () => { + inspectMock.mockResolvedValue({ + Version: { Index: "15" }, + Spec: { + TaskTemplate: { ForceUpdate: 3 }, + }, + }); + const db = createBaseDb("mariadb-test-1"); + + await buildMariadb(db as any); + + expect(updateMock).toHaveBeenCalledTimes(1); + const [updatePayload] = updateMock.mock.calls[0] as [any]; + expect(updatePayload.Networks).toEqual([ + { Target: "dokploy-network" }, + { Target: "custom-overlay" }, + ]); + expect(updatePayload.TaskTemplate.Networks).toEqual([ + { Target: "dokploy-network" }, + { Target: "custom-overlay" }, + ]); + }); + }); + + describe("buildRedis", () => { + it("attaches networks at both root and TaskTemplate when creating and updating service", async () => { + inspectMock.mockResolvedValue({ + Version: { Index: "20" }, + Spec: { + TaskTemplate: { ForceUpdate: 5 }, + }, + }); + const db = createBaseDb("redis-test-1"); + + await buildRedis(db as any); + + expect(updateMock).toHaveBeenCalledTimes(1); + const [updatePayload] = updateMock.mock.calls[0] as [any]; + expect(updatePayload.Networks).toEqual([ + { Target: "dokploy-network" }, + { Target: "custom-overlay" }, + ]); + expect(updatePayload.TaskTemplate.Networks).toEqual([ + { Target: "dokploy-network" }, + { Target: "custom-overlay" }, + ]); + }); + }); + + describe("buildMongo", () => { + it("attaches networks at both root and TaskTemplate when creating and updating service", async () => { + inspectMock.mockResolvedValue({ + Version: { Index: "25" }, + Spec: { + TaskTemplate: { ForceUpdate: 2 }, + }, + }); + const db = createBaseDb("mongo-test-1"); + + await buildMongo(db as any); + + expect(updateMock).toHaveBeenCalledTimes(1); + const [updatePayload] = updateMock.mock.calls[0] as [any]; + expect(updatePayload.Networks).toEqual([ + { Target: "dokploy-network" }, + { Target: "custom-overlay" }, + ]); + expect(updatePayload.TaskTemplate.Networks).toEqual([ + { Target: "dokploy-network" }, + { Target: "custom-overlay" }, + ]); + }); + }); + + describe("buildLibsql", () => { + it("attaches networks at both root and TaskTemplate when creating and updating service", async () => { + inspectMock.mockResolvedValue({ + Version: { Index: "30" }, + Spec: { + TaskTemplate: { ForceUpdate: 1 }, + }, + }); + const db = createBaseDb("libsql-test-1"); + + await buildLibsql(db as any); + + expect(updateMock).toHaveBeenCalledTimes(1); + const [updatePayload] = updateMock.mock.calls[0] as [any]; + expect(updatePayload.Networks).toEqual([ + { Target: "dokploy-network" }, + { Target: "custom-overlay" }, + ]); + expect(updatePayload.TaskTemplate.Networks).toEqual([ + { Target: "dokploy-network" }, + { Target: "custom-overlay" }, + ]); + }); + }); +}); diff --git a/packages/server/src/services/rollbacks.ts b/packages/server/src/services/rollbacks.ts index 8837ae912..be8bea178 100644 --- a/packages/server/src/services/rollbacks.ts +++ b/packages/server/src/services/rollbacks.ts @@ -286,6 +286,7 @@ const rollbackApplication = async ( serveraddress: rollbackRegistry?.registryUrl || "", }, Name: appName, + Networks: resolvedNetworks, TaskTemplate: { ContainerSpec: { HealthCheck, @@ -328,8 +329,10 @@ const rollbackApplication = async ( await service.update({ version: Number.parseInt(inspect.Version.Index), ...settings, + Networks: resolvedNetworks, TaskTemplate: { ...settings.TaskTemplate, + Networks: resolvedNetworks, ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1, }, }); diff --git a/packages/server/src/utils/builders/index.ts b/packages/server/src/utils/builders/index.ts index 6c8448000..1bd954b42 100644 --- a/packages/server/src/utils/builders/index.ts +++ b/packages/server/src/utils/builders/index.ts @@ -134,6 +134,7 @@ export const mechanizeDockerContainer = async ( const settings: CreateServiceOptions = { authconfig: authConfig, Name: appName, + Networks: resolvedNetworks, TaskTemplate: { ContainerSpec: { HealthCheck, @@ -181,8 +182,10 @@ export const mechanizeDockerContainer = async ( await service.update({ version: Number.parseInt(inspect.Version.Index), ...settings, + Networks: resolvedNetworks, TaskTemplate: { ...settings.TaskTemplate, + Networks: resolvedNetworks, ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1, }, }); diff --git a/packages/server/src/utils/databases/libsql.ts b/packages/server/src/utils/databases/libsql.ts index d3b2f9947..ec54116f7 100644 --- a/packages/server/src/utils/databases/libsql.ts +++ b/packages/server/src/utils/databases/libsql.ts @@ -86,6 +86,7 @@ export const buildLibsql = async (rawLibsql: LibsqlNested) => { const settings: CreateServiceOptions = { Name: appName, + Networks: resolvedNetworks, TaskTemplate: { ContainerSpec: { HealthCheck, @@ -156,6 +157,12 @@ export const buildLibsql = async (rawLibsql: LibsqlNested) => { await service.update({ version: Number.parseInt(inspect.Version.Index), ...settings, + Networks: resolvedNetworks, + TaskTemplate: { + ...settings.TaskTemplate, + Networks: resolvedNetworks, + ForceUpdate: (inspect.Spec.TaskTemplate?.ForceUpdate ?? 0) + 1, + }, }); } catch { await docker.createService(settings); diff --git a/packages/server/src/utils/databases/mariadb.ts b/packages/server/src/utils/databases/mariadb.ts index ae564504e..1a97ba8df 100644 --- a/packages/server/src/utils/databases/mariadb.ts +++ b/packages/server/src/utils/databases/mariadb.ts @@ -73,6 +73,7 @@ export const buildMariadb = async (rawMariadb: MariadbNested) => { const settings: CreateServiceOptions = { Name: appName, + Networks: resolvedNetworks, TaskTemplate: { ContainerSpec: { HealthCheck, @@ -127,8 +128,10 @@ export const buildMariadb = async (rawMariadb: MariadbNested) => { await service.update({ version: Number.parseInt(inspect.Version.Index), ...settings, + Networks: resolvedNetworks, TaskTemplate: { ...settings.TaskTemplate, + Networks: resolvedNetworks, ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1, }, }); diff --git a/packages/server/src/utils/databases/mongo.ts b/packages/server/src/utils/databases/mongo.ts index 67496aeaf..b0ffb4608 100644 --- a/packages/server/src/utils/databases/mongo.ts +++ b/packages/server/src/utils/databases/mongo.ts @@ -121,6 +121,7 @@ ${command ?? "wait $MONGOD_PID"}`; const settings: CreateServiceOptions = { Name: appName, + Networks: resolvedNetworks, TaskTemplate: { ContainerSpec: { HealthCheck, @@ -184,8 +185,10 @@ ${command ?? "wait $MONGOD_PID"}`; await service.update({ version: Number.parseInt(inspect.Version.Index), ...settings, + Networks: resolvedNetworks, TaskTemplate: { ...settings.TaskTemplate, + Networks: resolvedNetworks, ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1, }, }); diff --git a/packages/server/src/utils/databases/mysql.ts b/packages/server/src/utils/databases/mysql.ts index 56bb7ea4d..ece4d325f 100644 --- a/packages/server/src/utils/databases/mysql.ts +++ b/packages/server/src/utils/databases/mysql.ts @@ -79,6 +79,7 @@ export const buildMysql = async (rawMysql: MysqlNested) => { const settings: CreateServiceOptions = { Name: appName, + Networks: resolvedNetworks, TaskTemplate: { ContainerSpec: { HealthCheck, @@ -133,8 +134,10 @@ export const buildMysql = async (rawMysql: MysqlNested) => { await service.update({ version: Number.parseInt(inspect.Version.Index), ...settings, + Networks: resolvedNetworks, TaskTemplate: { ...settings.TaskTemplate, + Networks: resolvedNetworks, ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1, }, }); diff --git a/packages/server/src/utils/databases/postgres.ts b/packages/server/src/utils/databases/postgres.ts index 18392b7b5..0860b6746 100644 --- a/packages/server/src/utils/databases/postgres.ts +++ b/packages/server/src/utils/databases/postgres.ts @@ -72,6 +72,7 @@ export const buildPostgres = async (rawPostgres: PostgresNested) => { const settings: CreateServiceOptions = { Name: appName, + Networks: resolvedNetworks, TaskTemplate: { ContainerSpec: { HealthCheck, @@ -125,8 +126,10 @@ export const buildPostgres = async (rawPostgres: PostgresNested) => { await service.update({ version: Number.parseInt(inspect.Version.Index), ...settings, + Networks: resolvedNetworks, TaskTemplate: { ...settings.TaskTemplate, + Networks: resolvedNetworks, ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1, }, }); diff --git a/packages/server/src/utils/databases/redis.ts b/packages/server/src/utils/databases/redis.ts index 64c16657b..0ec3e09bb 100644 --- a/packages/server/src/utils/databases/redis.ts +++ b/packages/server/src/utils/databases/redis.ts @@ -70,6 +70,7 @@ export const buildRedis = async (rawRedis: RedisNested) => { const settings: CreateServiceOptions = { Name: appName, + Networks: resolvedNetworks, TaskTemplate: { ContainerSpec: { HealthCheck, @@ -132,8 +133,10 @@ export const buildRedis = async (rawRedis: RedisNested) => { await service.update({ version: Number.parseInt(inspect.Version.Index), ...settings, + Networks: resolvedNetworks, TaskTemplate: { ...settings.TaskTemplate, + Networks: resolvedNetworks, ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1, }, });