From 290228116aa509019b0cdac82d5a387f437e1da5 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Wed, 29 May 2024 23:36:59 -0600 Subject: [PATCH 1/2] v0.1.0 (#114) * feat: add schema for registry and routes * feat: add docker registry upload * feat: add show cluster * refactor: set the registry url in image in case we have a registry asociated * feat: add update registry and fix the docker url markup * chore: remove --advertise-ip on swarm script * refactor: remove listen address of swarm initialize * feat: add table to show nodes and add dropdown to add manager & workers * refactor: improve interface for cluster * refactor: improve UI * feat: add experimental swarm settings * refactor: remove comments * refactor: prettify json of each setting * refactor: add interface tooltip * refactor: delete static form self registry * refactor: allow to se a empty registry * fix: remove text area warnings * feat: add network swarm json * refactor: update ui * revert: go back to swarm init config * refactor: remove initialization on server, only on setup script * Update LICENSE.MD * feat: appearance theme support system config * refactor: remove logs * fix(README-ru): hyperlink-ed docs url * feat: (#107) webhook listener filter docker events based on image tag. Fixes #107 * refactor: simplify comparison docker tags * refactor: remove return in res status * refactor: prevent to updates download automatically * feat: support code editor (#105) * feat: support code editor * Update codeblock * refactor: remove unused class --------- Co-authored-by: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> * fix: select the right image from sourcetype (#109) * chore: bump minor version * fix: add redirect to https by default (#113) --------- Co-authored-by: hehehai Co-authored-by: Bayram Tagiev Co-authored-by: Paulo Santana <30875229+hikinine@users.noreply.github.com> --- server/utils/traefik/registry.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/server/utils/traefik/registry.ts b/server/utils/traefik/registry.ts index a08af834..2be46cb2 100644 --- a/server/utils/traefik/registry.ts +++ b/server/utils/traefik/registry.ts @@ -44,6 +44,7 @@ const createRegistryRouterConfig = async (registry: Registry) => { const routerConfig: HttpRouter = { rule: `Host(\`${registryUrl}\`)`, service: "dokploy-registry-service", + middlewares: ["redirect-to-https"], entryPoints: [ "web", ...(process.env.NODE_ENV === "production" ? ["websecure"] : []), From 7f13fd24eca0913f0c639a9b6f1e1dee71ae1f05 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 2 Jun 2024 21:24:56 -0600 Subject: [PATCH 2/2] v0.2.0 (#117) * feat: add schema for registry and routes * feat: add docker registry upload * feat: add show cluster * refactor: set the registry url in image in case we have a registry asociated * feat: add update registry and fix the docker url markup * chore: remove --advertise-ip on swarm script * refactor: remove listen address of swarm initialize * feat: add table to show nodes and add dropdown to add manager & workers * refactor: improve interface for cluster * refactor: improve UI * feat: add experimental swarm settings * refactor: remove comments * refactor: prettify json of each setting * refactor: add interface tooltip * refactor: delete static form self registry * refactor: allow to se a empty registry * fix: remove text area warnings * feat: add network swarm json * refactor: update ui * revert: go back to swarm init config * refactor: remove initialization on server, only on setup script * Update LICENSE.MD * feat: appearance theme support system config * refactor: remove logs * fix(README-ru): hyperlink-ed docs url * feat: (#107) webhook listener filter docker events based on image tag. Fixes #107 * refactor: simplify comparison docker tags * refactor: remove return in res status * refactor: prevent to updates download automatically * feat: support code editor (#105) * feat: support code editor * Update codeblock * refactor: remove unused class --------- Co-authored-by: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> * fix: select the right image from sourcetype (#109) * chore: bump minor version * fix: add redirect to https by default (#113) * Create FUNDING.yml * Docker compose support (#111) * feat(WIP): compose implementation * feat: add volumes, networks, services name hash generate * feat: add compose config test unique * feat: add tests for each unique config * feat: implement lodash for docker compose parsing * feat: add tests for generating compose file * refactor: implement logs docker compose * refactor: composeFile set not empty * feat: implement providers for compose deployments * feat: add Files volumes to compose * feat: add stop compose button * refactor: change strategie of building compose * feat: create .env file in composepath * refactor: simplify git and github function * chore: update deps * refactor: update migrations and add badge to recognize compose type * chore: update lock yaml * refactor: use code editor * feat: add monitoring for app types * refactor: reset stats on change appName * refactor: add option to clean monitoring folder * feat: show current command that will run * feat: add prefix * fix: add missing types * refactor: add docker provider and expose by default as false * refactor: customize error page * refactor: unified deployments to be a single one * feat: add vitest to ci/cd * revert: back to initial version * refactor: add maxconcurrency vitest * refactor: add pool forks to vitest * feat: add pocketbase template * fix: update path resolution compose * removed * feat: add template pocketbase * feat: add pocketbase template * feat: add support button * feat: add plausible template * feat: add calcom template * feat: add version to each template * feat: add code editor to enviroment variables and swarm settings json * refactor: add loader when download the image * fix: use base64 to generate keys plausible * feat: add recognized domain names by enviroment compose * refactor: show alert to redeploy in each card advanced tab * refactor: add validation to prevent create compose if not have permissions * chore: add templates section to contributing * chore: add example contributing * chore: add recomendation to show variables * chore: add video to contributing templates * chore: bump version --------- Co-authored-by: hehehai Co-authored-by: Bayram Tagiev Co-authored-by: Paulo Santana <30875229+hikinine@users.noreply.github.com> --- .github/FUNDING.yml | 13 + .github/workflows/pull-request.yml | 2 + CONTRIBUTING.md | 89 + __test__/compose/compose.test.ts | 476 +++ __test__/compose/config/config-root.test.ts | 178 ++ .../compose/config/config-service.test.ts | 197 ++ __test__/compose/config/config.test.ts | 249 ++ __test__/compose/network/network-root.test.ts | 281 ++ .../compose/network/network-service.test.ts | 181 ++ __test__/compose/network/network.test.ts | 254 ++ __test__/compose/secrets/secret-root.test.ts | 103 + .../compose/secrets/secret-services.test.ts | 113 + __test__/compose/secrets/secret.test.ts | 159 + .../service/service-container-name.test.ts | 59 + .../service/service-depends-on.test.ts | 150 + .../compose/service/service-extends.test.ts | 131 + .../compose/service/service-links.test.ts | 76 + .../compose/service/service-names.test.ts | 49 + __test__/compose/service/service.test.ts | 375 +++ .../service/sevice-volumes-from.test.ts | 76 + __test__/compose/volume/volume-2.test.ts | 1120 +++++++ __test__/compose/volume/volume-root.test.ts | 195 ++ .../compose/volume/volume-services.test.ts | 81 + __test__/compose/volume/volume.test.ts | 288 ++ __test__/vitest.config.ts | 16 + .../cluster/modify-swarm-settings.tsx | 42 +- .../cluster/show-cluster-settings.tsx | 5 + .../application/advanced/ports/show-port.tsx | 7 +- .../show-application-advanced-settings.tsx | 5 + .../advanced/volumes/add-volumes.tsx | 87 +- .../advanced/volumes/show-volumes.tsx | 24 +- .../advanced/volumes/update-volume.tsx | 263 ++ .../application/environment/show.tsx | 8 +- .../compose/advanced/add-command.tsx | 133 + .../compose/advanced/show-volumes.tsx | 133 + .../dashboard/compose/delete-compose.tsx | 63 + .../deployments/cancel-queues-compose.tsx | 61 + .../deployments/refresh-token-compose.tsx | 60 + .../deployments/show-deployment-compose.tsx | 69 + .../deployments/show-deployments-compose.tsx | 119 + .../dashboard/compose/enviroment/show.tsx | 122 + .../dashboard/compose/general/actions.tsx | 121 + .../compose/general/compose-file-editor.tsx | 142 + .../compose/general/deploy-compose.tsx | 78 + .../generic/save-git-provider-compose.tsx | 255 ++ .../generic/save-github-provider-compose.tsx | 310 ++ .../compose/general/generic/show.tsx | 97 + .../compose/general/randomize-compose.tsx | 98 + .../compose/general/rebuild-compose.tsx | 85 + components/dashboard/compose/general/show.tsx | 47 + .../compose/general/stop-compose.tsx | 79 + components/dashboard/compose/logs/show.tsx | 88 + .../dashboard/compose/monitoring/show.tsx | 85 + .../dashboard/compose/update-compose.tsx | 159 + .../advanced/show-mariadb-resources.tsx | 419 +-- .../environment/show-mariadb-environment.tsx | 9 +- .../mariadb/volumes/show-volumes.tsx | 24 +- .../mongo/advanced/show-mongo-resources.tsx | 423 +-- .../environment/show-mongo-environment.tsx | 9 +- .../dashboard/mongo/volumes/show-volumes.tsx | 24 +- .../dashboard/monitoring/docker/show.tsx | 88 +- .../mysql/advanced/show-mysql-resources.tsx | 419 +-- .../environment/show-mysql-environment.tsx | 9 +- .../dashboard/mysql/volumes/show-volumes.tsx | 24 +- .../advanced/show-postgres-resources.tsx | 5 + .../environment/show-postgres-environment.tsx | 9 +- .../postgres/volumes/show-volumes.tsx | 24 +- components/dashboard/project/add-compose.tsx | 186 ++ components/dashboard/project/add-template.tsx | 209 ++ components/dashboard/projects/show.tsx | 9 +- .../redis/advanced/show-redis-resources.tsx | 419 +-- .../environment/show-redis-environment.tsx | 11 +- .../dashboard/redis/volumes/show-volumes.tsx | 8 +- components/dashboard/settings/web-server.tsx | 17 + components/layouts/navbar.tsx | 19 +- components/shared/code-editor.tsx | 14 +- components/support/show-support.tsx | 35 + docker/feat.sh | 10 + docker/push.sh | 3 +- drizzle/0014_same_hammerhead.sql | 57 + drizzle/meta/0014_snapshot.json | 2607 +++++++++++++++++ drizzle/meta/_journal.json | 7 + next.config.mjs | 30 + package.json | 22 +- pages/_error.tsx | 113 +- pages/api/deploy/[refreshToken].ts | 21 +- pages/api/deploy/compose/[refreshToken].ts | 83 + pages/dashboard/project/[projectId].tsx | 40 +- .../services/application/[applicationId].tsx | 1 - .../services/compose/[composeId].tsx | 245 ++ pnpm-lock.yaml | 1272 +++++++- public/templates/calcom.jpg | Bin 0 -> 17043 bytes public/templates/plausible.svg | 1 + public/templates/pocketbase.svg | 9 + server/api/root.ts | 2 + server/api/routers/application.ts | 2 + server/api/routers/compose.ts | 278 ++ server/api/routers/deployment.ts | 26 +- server/api/routers/mount.ts | 24 +- server/api/routers/project.ts | 8 + server/api/routers/settings.ts | 7 +- server/api/services/compose.ts | 215 ++ server/api/services/deployment.ts | 91 +- server/api/services/docker.ts | 3 +- server/api/services/mount.ts | 8 +- server/api/services/project.ts | 11 + server/api/services/settings.ts | 7 +- server/constants/index.ts | 1 + server/db/schema/compose.ts | 109 + server/db/schema/deployment.ts | 45 +- server/db/schema/index.ts | 1 + server/db/schema/mount.ts | 23 +- server/db/schema/project.ts | 2 + server/queues/deployments-queue.ts | 66 +- server/setup/traefik-setup.ts | 28 +- server/utils/builders/compose.ts | 117 + server/utils/docker/compose.ts | 45 + server/utils/docker/compose/configs.ts | 73 + server/utils/docker/compose/network.ts | 72 + server/utils/docker/compose/secrets.ts | 68 + server/utils/docker/compose/service.ts | 90 + server/utils/docker/compose/volume.ts | 78 + server/utils/docker/types.ts | 879 ++++++ server/utils/docker/utils.ts | 29 +- server/utils/filesystem/directory.ts | 16 +- server/utils/process/spawnAsync.ts | 92 +- server/utils/providers/git.ts | 19 +- server/utils/providers/github.ts | 31 +- server/utils/providers/raw.ts | 28 + server/wss/docker-stats.ts | 14 +- styles/globals.css | 32 + templates/calcom/docker-compose.yml | 37 + templates/calcom/index.ts | 21 + templates/plausible/docker-compose.yml | 57 + templates/plausible/index.ts | 66 + templates/pocketbase/docker-compose.yml | 21 + templates/pocketbase/index.ts | 22 + templates/templates.ts | 50 + templates/types/templates-data.type.ts | 67 + templates/utils/index.ts | 68 + 140 files changed, 16530 insertions(+), 1209 deletions(-) create mode 100644 .github/FUNDING.yml create mode 100644 __test__/compose/compose.test.ts create mode 100644 __test__/compose/config/config-root.test.ts create mode 100644 __test__/compose/config/config-service.test.ts create mode 100644 __test__/compose/config/config.test.ts create mode 100644 __test__/compose/network/network-root.test.ts create mode 100644 __test__/compose/network/network-service.test.ts create mode 100644 __test__/compose/network/network.test.ts create mode 100644 __test__/compose/secrets/secret-root.test.ts create mode 100644 __test__/compose/secrets/secret-services.test.ts create mode 100644 __test__/compose/secrets/secret.test.ts create mode 100644 __test__/compose/service/service-container-name.test.ts create mode 100644 __test__/compose/service/service-depends-on.test.ts create mode 100644 __test__/compose/service/service-extends.test.ts create mode 100644 __test__/compose/service/service-links.test.ts create mode 100644 __test__/compose/service/service-names.test.ts create mode 100644 __test__/compose/service/service.test.ts create mode 100644 __test__/compose/service/sevice-volumes-from.test.ts create mode 100644 __test__/compose/volume/volume-2.test.ts create mode 100644 __test__/compose/volume/volume-root.test.ts create mode 100644 __test__/compose/volume/volume-services.test.ts create mode 100644 __test__/compose/volume/volume.test.ts create mode 100644 __test__/vitest.config.ts create mode 100644 components/dashboard/application/advanced/volumes/update-volume.tsx create mode 100644 components/dashboard/compose/advanced/add-command.tsx create mode 100644 components/dashboard/compose/advanced/show-volumes.tsx create mode 100644 components/dashboard/compose/delete-compose.tsx create mode 100644 components/dashboard/compose/deployments/cancel-queues-compose.tsx create mode 100644 components/dashboard/compose/deployments/refresh-token-compose.tsx create mode 100644 components/dashboard/compose/deployments/show-deployment-compose.tsx create mode 100644 components/dashboard/compose/deployments/show-deployments-compose.tsx create mode 100644 components/dashboard/compose/enviroment/show.tsx create mode 100644 components/dashboard/compose/general/actions.tsx create mode 100644 components/dashboard/compose/general/compose-file-editor.tsx create mode 100644 components/dashboard/compose/general/deploy-compose.tsx create mode 100644 components/dashboard/compose/general/generic/save-git-provider-compose.tsx create mode 100644 components/dashboard/compose/general/generic/save-github-provider-compose.tsx create mode 100644 components/dashboard/compose/general/generic/show.tsx create mode 100644 components/dashboard/compose/general/randomize-compose.tsx create mode 100644 components/dashboard/compose/general/rebuild-compose.tsx create mode 100644 components/dashboard/compose/general/show.tsx create mode 100644 components/dashboard/compose/general/stop-compose.tsx create mode 100644 components/dashboard/compose/logs/show.tsx create mode 100644 components/dashboard/compose/monitoring/show.tsx create mode 100644 components/dashboard/compose/update-compose.tsx create mode 100644 components/dashboard/project/add-compose.tsx create mode 100644 components/dashboard/project/add-template.tsx create mode 100644 components/support/show-support.tsx create mode 100644 docker/feat.sh create mode 100644 drizzle/0014_same_hammerhead.sql create mode 100644 drizzle/meta/0014_snapshot.json create mode 100644 pages/api/deploy/compose/[refreshToken].ts create mode 100644 pages/dashboard/project/[projectId]/services/compose/[composeId].tsx create mode 100644 public/templates/calcom.jpg create mode 100644 public/templates/plausible.svg create mode 100644 public/templates/pocketbase.svg create mode 100644 server/api/routers/compose.ts create mode 100644 server/api/services/compose.ts create mode 100644 server/db/schema/compose.ts create mode 100644 server/utils/builders/compose.ts create mode 100644 server/utils/docker/compose.ts create mode 100644 server/utils/docker/compose/configs.ts create mode 100644 server/utils/docker/compose/network.ts create mode 100644 server/utils/docker/compose/secrets.ts create mode 100644 server/utils/docker/compose/service.ts create mode 100644 server/utils/docker/compose/volume.ts create mode 100644 server/utils/docker/types.ts create mode 100644 server/utils/providers/raw.ts create mode 100644 templates/calcom/docker-compose.yml create mode 100644 templates/calcom/index.ts create mode 100644 templates/plausible/docker-compose.yml create mode 100644 templates/plausible/index.ts create mode 100644 templates/pocketbase/docker-compose.yml create mode 100644 templates/pocketbase/index.ts create mode 100644 templates/templates.ts create mode 100644 templates/types/templates-data.type.ts create mode 100644 templates/utils/index.ts diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..97379020 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,13 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # +open_collective: dokploy +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 02d9bce9..36491813 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -30,6 +30,8 @@ jobs: run: pnpm install - name: Run Build run: pnpm build + - name: Run Tests + run: pnpm run test build-and-push-docker-on-push: if: github.event_name == 'push' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 67a8e3ec..8686b98a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -151,3 +151,92 @@ curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.32.1/pack-v0. - Once your pull request is merged, you will be automatically added as a contributor to the project. Thank you for your contribution! + + + + + +## Templates + +To add a new template, go to `templates` folder and create a new folder with the name of the template. + +Let's take the example of `plausible` template. + +1. create a folder in `templates/plausible` +2. create a `docker-compose.yml` file inside the folder with the content of compose. +3. create a `index.ts` file inside the folder with the following code as base: +4. When creating a pull request, please provide a video of the template working in action. + +```typescript +// EXAMPLE +import { + generateHash, + generateRandomDomain, + type Template, + type Schema, +} from "../utils"; + + +export function generate(schema: Schema): Template { + + // do your stuff here, like create a new domain, generate random passwords, mounts. + const mainServiceHash = generateHash(schema.projectName); + const randomDomain = generateRandomDomain(schema); + const secretBase = generateBase64(64); + const toptKeyBase = generateBase64(32); + + const envs = [ +// If you want to show a domain in the UI, please add the prefix _HOST at the end of the variable name. + `PLAUSIBLE_HOST=${randomDomain}`, + "PLAUSIBLE_PORT=8000", + `BASE_URL=http://${randomDomain}`, + `SECRET_KEY_BASE=${secretBase}`, + `TOTP_VAULT_KEY=${toptKeyBase}`, + `HASH=${mainServiceHash}`, + ]; + + const mounts: Template["mounts"] = [ + { + mountPath: "./clickhouse/clickhouse-config.xml", + content: `some content......`, + }, + ]; + + return { + envs, + mounts, + }; +} +``` + +4. Now you need to add the information about the template to the `templates/templates.ts` is a object with the following properties: + +**Make sure the id of the template is the same as the folder name and don't have any spaces, only slugified names and lowercase.** + +```typescript +{ + id: "plausible", + name: "Plausible", + version: "v2.1.0", + description: + "Plausible is a open source, self-hosted web analytics platform that lets you track website traffic and user behavior.", + logo: "plausible.svg", // we defined the name and the extension of the logo + links: { + github: "https://github.com/plausible/plausible", + website: "https://plausible.io/", + docs: "https://plausible.io/docs", + }, + tags: ["analytics"], + load: () => import("./plausible/index").then((m) => m.generate), +}, +``` + +5. Add the logo or image of the template to `public/templates/plausible.svg` + + +### Recomendations +- Use the same name of the folder as the id of the template. +- The logo should be in the public folder. +- If you want to show a domain in the UI, please add the prefix _HOST at the end of the variable name. +- Test first on a vps or a server to make sure the template works. + diff --git a/__test__/compose/compose.test.ts b/__test__/compose/compose.test.ts new file mode 100644 index 00000000..675cb772 --- /dev/null +++ b/__test__/compose/compose.test.ts @@ -0,0 +1,476 @@ +import { expect, test } from "vitest"; +import { load } from "js-yaml"; +import { addPrefixToAllProperties } from "@/server/utils/docker/compose"; +import type { ComposeSpecification } from "@/server/utils/docker/types"; + +const composeFile1 = ` +version: "3.8" + +services: + web: + image: nginx:latest + container_name: web_container + depends_on: + - app + networks: + - frontend + volumes_from: + - data + links: + - db + extends: + service: base_service + configs: + - source: web_config + + app: + image: node:14 + networks: + - backend + - frontend + + db: + image: postgres:13 + networks: + - backend + + data: + image: busybox + volumes: + - /data + + base_service: + image: base:latest + +networks: + frontend: + driver: bridge + backend: + driver: bridge + +volumes: + web_data: + driver: local + +configs: + web_config: + file: ./web_config.yml + +secrets: + db_password: + file: ./db_password.txt +`; + +const expectedComposeFile1 = load(` +version: "3.8" + +services: + web-testhash: + image: nginx:latest + container_name: web_container-testhash + depends_on: + - app-testhash + networks: + - frontend-testhash + volumes_from: + - data-testhash + links: + - db-testhash + extends: + service: base_service-testhash + configs: + - source: web_config-testhash + + app-testhash: + image: node:14 + networks: + - backend-testhash + - frontend-testhash + + db-testhash: + image: postgres:13 + networks: + - backend-testhash + + data-testhash: + image: busybox + volumes: + - /data + + base_service-testhash: + image: base:latest + +networks: + frontend-testhash: + driver: bridge + backend-testhash: + driver: bridge + +volumes: + web_data-testhash: + driver: local + +configs: + web_config-testhash: + file: ./web_config.yml + +secrets: + db_password-testhash: + file: ./db_password.txt +`) as ComposeSpecification; + +test("Add prefix to all properties in compose file 1", () => { + const composeData = load(composeFile1) as ComposeSpecification; + const prefix = "testhash"; + + const updatedComposeData = addPrefixToAllProperties(composeData, prefix); + + expect(updatedComposeData).toEqual(expectedComposeFile1); +}); + +const composeFile2 = ` +version: "3.8" + +services: + frontend: + image: nginx:latest + depends_on: + - backend + networks: + - public + volumes_from: + - logs + links: + - cache + extends: + service: shared_service + secrets: + - db_password + + backend: + image: node:14 + networks: + - private + - public + + cache: + image: redis:latest + networks: + - private + + logs: + image: busybox + volumes: + - /logs + + shared_service: + image: shared:latest + +networks: + public: + driver: bridge + private: + driver: bridge + +volumes: + logs: + driver: local + +configs: + app_config: + file: ./app_config.yml + +secrets: + db_password: + file: ./db_password.txt +`; + +const expectedComposeFile2 = load(` +version: "3.8" + +services: + frontend-testhash: + image: nginx:latest + depends_on: + - backend-testhash + networks: + - public-testhash + volumes_from: + - logs-testhash + links: + - cache-testhash + extends: + service: shared_service-testhash + secrets: + - db_password-testhash + + backend-testhash: + image: node:14 + networks: + - private-testhash + - public-testhash + + cache-testhash: + image: redis:latest + networks: + - private-testhash + + logs-testhash: + image: busybox + volumes: + - /logs + + shared_service-testhash: + image: shared:latest + +networks: + public-testhash: + driver: bridge + private-testhash: + driver: bridge + +volumes: + logs-testhash: + driver: local + +configs: + app_config-testhash: + file: ./app_config.yml + +secrets: + db_password-testhash: + file: ./db_password.txt +`) as ComposeSpecification; + +test("Add prefix to all properties in compose file 2", () => { + const composeData = load(composeFile2) as ComposeSpecification; + const prefix = "testhash"; + + const updatedComposeData = addPrefixToAllProperties(composeData, prefix); + + expect(updatedComposeData).toEqual(expectedComposeFile2); +}); + +const composeFile3 = ` +version: "3.8" + +services: + service_a: + image: service_a:latest + depends_on: + - service_b + networks: + - net_a + volumes_from: + - data_volume + links: + - service_c + extends: + service: common_service + configs: + - source: service_a_config + + service_b: + image: service_b:latest + networks: + - net_b + - net_a + + service_c: + image: service_c:latest + networks: + - net_b + + data_volume: + image: busybox + volumes: + - /data + + common_service: + image: common:latest + +networks: + net_a: + driver: bridge + net_b: + driver: bridge + +volumes: + data_volume: + driver: local + +configs: + service_a_config: + file: ./service_a_config.yml + +secrets: + service_secret: + file: ./service_secret.txt +`; + +const expectedComposeFile3 = load(` +version: "3.8" + +services: + service_a-testhash: + image: service_a:latest + depends_on: + - service_b-testhash + networks: + - net_a-testhash + volumes_from: + - data_volume-testhash + links: + - service_c-testhash + extends: + service: common_service-testhash + configs: + - source: service_a_config-testhash + + service_b-testhash: + image: service_b:latest + networks: + - net_b-testhash + - net_a-testhash + + service_c-testhash: + image: service_c:latest + networks: + - net_b-testhash + + data_volume-testhash: + image: busybox + volumes: + - /data + + common_service-testhash: + image: common:latest + +networks: + net_a-testhash: + driver: bridge + net_b-testhash: + driver: bridge + +volumes: + data_volume-testhash: + driver: local + +configs: + service_a_config-testhash: + file: ./service_a_config.yml + +secrets: + service_secret-testhash: + file: ./service_secret.txt +`) as ComposeSpecification; + +test("Add prefix to all properties in compose file 3", () => { + const composeData = load(composeFile3) as ComposeSpecification; + const prefix = "testhash"; + + const updatedComposeData = addPrefixToAllProperties(composeData, prefix); + + expect(updatedComposeData).toEqual(expectedComposeFile3); +}); + +const composeFile = ` +version: "3.8" + +services: + plausible_db: + image: postgres:16-alpine + restart: always + volumes: + - db-data:/var/lib/postgresql/data + environment: + - POSTGRES_PASSWORD=postgres + + plausible_events_db: + image: clickhouse/clickhouse-server:24.3.3.102-alpine + restart: always + volumes: + - event-data:/var/lib/clickhouse + - event-logs:/var/log/clickhouse-server + - ./clickhouse/clickhouse-config.xml:/etc/clickhouse-server/config.d/logging.xml:ro + - ./clickhouse/clickhouse-user-config.xml:/etc/clickhouse-server/users.d/logging.xml:ro + ulimits: + nofile: + soft: 262144 + hard: 262144 + + plausible: + image: ghcr.io/plausible/community-edition:v2.1.0 + restart: always + command: sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh run" + depends_on: + - plausible_db + - plausible_events_db + ports: + - 127.0.0.1:8000:8000 + env_file: + - plausible-conf.env + +volumes: + db-data: + driver: local + event-data: + driver: local + event-logs: + driver: local +`; + +const expectedComposeFile = load(` +version: "3.8" + +services: + plausible_db-testhash: + image: postgres:16-alpine + restart: always + volumes: + - db-data-testhash:/var/lib/postgresql/data + environment: + - POSTGRES_PASSWORD=postgres + + plausible_events_db-testhash: + image: clickhouse/clickhouse-server:24.3.3.102-alpine + restart: always + volumes: + - event-data-testhash:/var/lib/clickhouse + - event-logs-testhash:/var/log/clickhouse-server + - ./clickhouse/clickhouse-config.xml:/etc/clickhouse-server/config.d/logging.xml:ro + - ./clickhouse/clickhouse-user-config.xml:/etc/clickhouse-server/users.d/logging.xml:ro + ulimits: + nofile: + soft: 262144 + hard: 262144 + + plausible-testhash: + image: ghcr.io/plausible/community-edition:v2.1.0 + restart: always + command: sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh run" + depends_on: + - plausible_db-testhash + - plausible_events_db-testhash + ports: + - 127.0.0.1:8000:8000 + env_file: + - plausible-conf.env + +volumes: + db-data-testhash: + driver: local + event-data-testhash: + driver: local + event-logs-testhash: + driver: local +`) as ComposeSpecification; + +test("Add prefix to all properties in Plausible compose file", () => { + const composeData = load(composeFile) as ComposeSpecification; + const prefix = "testhash"; + + const updatedComposeData = addPrefixToAllProperties(composeData, prefix); + + expect(updatedComposeData).toEqual(expectedComposeFile); +}); diff --git a/__test__/compose/config/config-root.test.ts b/__test__/compose/config/config-root.test.ts new file mode 100644 index 00000000..88e8b357 --- /dev/null +++ b/__test__/compose/config/config-root.test.ts @@ -0,0 +1,178 @@ +import { generateRandomHash } from "@/server/utils/docker/compose"; +import { addPrefixToConfigsRoot } from "@/server/utils/docker/compose/configs"; +import type { ComposeSpecification } from "@/server/utils/docker/types"; +import { load } from "js-yaml"; +import { expect, test } from "vitest"; + +test("Generate random hash with 8 characters", () => { + const hash = generateRandomHash(); + + expect(hash).toBeDefined(); + expect(hash.length).toBe(8); +}); + +const composeFile = ` +version: "3.8" + +services: + web: + image: nginx:latest + +configs: + web-config: + file: ./web-config.yml +`; + +test("Add prefix to configs in root property", () => { + const composeData = load(composeFile) as ComposeSpecification; + + const prefix = generateRandomHash(); + + if (!composeData?.configs) { + return; + } + const configs = addPrefixToConfigsRoot(composeData.configs, prefix); + + expect(configs).toBeDefined(); + for (const configKey of Object.keys(configs)) { + expect(configKey).toContain(`-${prefix}`); + expect(configs[configKey]).toBeDefined(); + } +}); + +const composeFileMultipleConfigs = ` +version: "3.8" + +services: + web: + image: nginx:latest + configs: + - source: web-config + target: /etc/nginx/nginx.conf + - source: another-config + target: /etc/nginx/another.conf + +configs: + web-config: + file: ./web-config.yml + another-config: + file: ./another-config.yml +`; + +test("Add prefix to multiple configs in root property", () => { + const composeData = load(composeFileMultipleConfigs) as ComposeSpecification; + + const prefix = generateRandomHash(); + + if (!composeData?.configs) { + return; + } + const configs = addPrefixToConfigsRoot(composeData.configs, prefix); + + expect(configs).toBeDefined(); + for (const configKey of Object.keys(configs)) { + expect(configKey).toContain(`-${prefix}`); + expect(configs[configKey]).toBeDefined(); + } + expect(configs).toHaveProperty(`web-config-${prefix}`); + expect(configs).toHaveProperty(`another-config-${prefix}`); +}); + +const composeFileDifferentProperties = ` +version: "3.8" + +services: + web: + image: nginx:latest + +configs: + web-config: + file: ./web-config.yml + special-config: + external: true +`; + +test("Add prefix to configs with different properties in root property", () => { + const composeData = load( + composeFileDifferentProperties, + ) as ComposeSpecification; + + const prefix = generateRandomHash(); + + if (!composeData?.configs) { + return; + } + const configs = addPrefixToConfigsRoot(composeData.configs, prefix); + + expect(configs).toBeDefined(); + for (const configKey of Object.keys(configs)) { + expect(configKey).toContain(`-${prefix}`); + expect(configs[configKey]).toBeDefined(); + } + expect(configs).toHaveProperty(`web-config-${prefix}`); + expect(configs).toHaveProperty(`special-config-${prefix}`); +}); + +const composeFileConfigRoot = ` +version: "3.8" + +services: + web: + image: nginx:latest + + app: + image: node:latest + + db: + image: postgres:latest + +configs: + web_config: + file: ./web-config.yml + + app_config: + file: ./app-config.json + + db_config: + file: ./db-config.yml +`; + +// Expected compose file con el prefijo `testhash` +const expectedComposeFileConfigRoot = load(` +version: "3.8" + +services: + web: + image: nginx:latest + + app: + image: node:latest + + db: + image: postgres:latest + +configs: + web_config-testhash: + file: ./web-config.yml + + app_config-testhash: + file: ./app-config.json + + db_config-testhash: + file: ./db-config.yml +`) as ComposeSpecification; + +test("Add prefix to configs in root property", () => { + const composeData = load(composeFileConfigRoot) as ComposeSpecification; + + const prefix = "testhash"; + + if (!composeData?.configs) { + return; + } + const configs = addPrefixToConfigsRoot(composeData.configs, prefix); + const updatedComposeData = { ...composeData, configs }; + + // Verificar que el resultado coincide con el archivo esperado + expect(updatedComposeData).toEqual(expectedComposeFileConfigRoot); +}); diff --git a/__test__/compose/config/config-service.test.ts b/__test__/compose/config/config-service.test.ts new file mode 100644 index 00000000..df2bb417 --- /dev/null +++ b/__test__/compose/config/config-service.test.ts @@ -0,0 +1,197 @@ +import { generateRandomHash } from "@/server/utils/docker/compose"; +import { addPrefixToConfigsInServices } from "@/server/utils/docker/compose/configs"; +import type { ComposeSpecification } from "@/server/utils/docker/types"; +import { load } from "js-yaml"; +import { expect, test } from "vitest"; + +const composeFile = ` +version: "3.8" + +services: + web: + image: nginx:latest + configs: + - source: web-config + target: /etc/nginx/nginx.conf + +configs: + web-config: + file: ./web-config.yml +`; + +test("Add prefix to configs in services", () => { + const composeData = load(composeFile) as ComposeSpecification; + + const prefix = generateRandomHash(); + + if (!composeData?.services) { + return; + } + const services = addPrefixToConfigsInServices(composeData.services, prefix); + const actualComposeData = { ...composeData, services }; + + expect(actualComposeData.services?.web?.configs).toContainEqual({ + source: `web-config-${prefix}`, + target: "/etc/nginx/nginx.conf", + }); +}); + +const composeFileSingleServiceConfig = ` +version: "3.8" + +services: + web: + image: nginx:latest + configs: + - source: web-config + target: /etc/nginx/nginx.conf + +configs: + web-config: + file: ./web-config.yml +`; + +test("Add prefix to configs in services with single config", () => { + const composeData = load( + composeFileSingleServiceConfig, + ) as ComposeSpecification; + + const prefix = generateRandomHash(); + + if (!composeData?.services) { + return; + } + const services = addPrefixToConfigsInServices(composeData.services, prefix); + + expect(services).toBeDefined(); + for (const serviceKey of Object.keys(services)) { + const serviceConfigs = services?.[serviceKey]?.configs; + if (serviceConfigs) { + for (const config of serviceConfigs) { + if (typeof config === "object") { + expect(config.source).toContain(`-${prefix}`); + } + } + } + } +}); + +const composeFileMultipleServicesConfigs = ` +version: "3.8" + +services: + web: + image: nginx:latest + configs: + - source: web-config + target: /etc/nginx/nginx.conf + - source: common-config + target: /etc/nginx/common.conf + + app: + image: node:14 + configs: + - source: app-config + target: /usr/src/app/config.json + - source: common-config + target: /usr/src/app/common.json + +configs: + web-config: + file: ./web-config.yml + app-config: + file: ./app-config.json + common-config: + file: ./common-config.yml +`; + +test("Add prefix to configs in services with multiple configs", () => { + const composeData = load( + composeFileMultipleServicesConfigs, + ) as ComposeSpecification; + + const prefix = generateRandomHash(); + + if (!composeData?.services) { + return; + } + const services = addPrefixToConfigsInServices(composeData.services, prefix); + + expect(services).toBeDefined(); + for (const serviceKey of Object.keys(services)) { + const serviceConfigs = services?.[serviceKey]?.configs; + if (serviceConfigs) { + for (const config of serviceConfigs) { + if (typeof config === "object") { + expect(config.source).toContain(`-${prefix}`); + } + } + } + } +}); + +const composeFileConfigServices = ` +version: "3.8" + +services: + web: + image: nginx:latest + configs: + - source: web_config + target: /etc/nginx/nginx.conf + + app: + image: node:latest + configs: + - source: app_config + target: /usr/src/app/config.json + + db: + image: postgres:latest + configs: + - source: db_config + target: /etc/postgresql/postgresql.conf + +`; + +// Expected compose file con el prefijo `testhash` +const expectedComposeFileConfigServices = load(` +version: "3.8" + +services: + web: + image: nginx:latest + configs: + - source: web_config-testhash + target: /etc/nginx/nginx.conf + + app: + image: node:latest + configs: + - source: app_config-testhash + target: /usr/src/app/config.json + + db: + image: postgres:latest + configs: + - source: db_config-testhash + target: /etc/postgresql/postgresql.conf + +`) as ComposeSpecification; + +test("Add prefix to configs in services", () => { + const composeData = load(composeFileConfigServices) as ComposeSpecification; + + const prefix = "testhash"; + + if (!composeData?.services) { + return; + } + const updatedComposeData = addPrefixToConfigsInServices( + composeData.services, + prefix, + ); + const actualComposeData = { ...composeData, services: updatedComposeData }; + + expect(actualComposeData).toEqual(expectedComposeFileConfigServices); +}); diff --git a/__test__/compose/config/config.test.ts b/__test__/compose/config/config.test.ts new file mode 100644 index 00000000..8f17ac86 --- /dev/null +++ b/__test__/compose/config/config.test.ts @@ -0,0 +1,249 @@ +import { generateRandomHash } from "@/server/utils/docker/compose"; +import { + addPrefixToAllConfigs, + addPrefixToConfigsRoot, +} from "@/server/utils/docker/compose/configs"; +import type { ComposeSpecification } from "@/server/utils/docker/types"; +import { load } from "js-yaml"; +import { expect, test } from "vitest"; + +test("Generate random hash with 8 characters", () => { + const hash = generateRandomHash(); + + expect(hash).toBeDefined(); + expect(hash.length).toBe(8); +}); + +const composeFileCombinedConfigs = ` +version: "3.8" + +services: + web: + image: nginx:latest + configs: + - source: web_config + target: /etc/nginx/nginx.conf + + app: + image: node:14 + configs: + - source: app_config + target: /usr/src/app/config.json + + db: + image: postgres:13 + configs: + - source: db_config + target: /etc/postgresql/postgresql.conf + +configs: + web_config: + file: ./web-config.yml + + app_config: + file: ./app-config.json + + db_config: + file: ./db-config.yml +`; + +const expectedComposeFileCombinedConfigs = load(` +version: "3.8" + +services: + web: + image: nginx:latest + configs: + - source: web_config-testhash + target: /etc/nginx/nginx.conf + + app: + image: node:14 + configs: + - source: app_config-testhash + target: /usr/src/app/config.json + + db: + image: postgres:13 + configs: + - source: db_config-testhash + target: /etc/postgresql/postgresql.conf + +configs: + web_config-testhash: + file: ./web-config.yml + + app_config-testhash: + file: ./app-config.json + + db_config-testhash: + file: ./db-config.yml +`) as ComposeSpecification; + +test("Add prefix to all configs in root and services", () => { + const composeData = load(composeFileCombinedConfigs) as ComposeSpecification; + + const prefix = "testhash"; + + const updatedComposeData = addPrefixToAllConfigs(composeData, prefix); + + expect(updatedComposeData).toEqual(expectedComposeFileCombinedConfigs); +}); + +const composeFileWithEnvAndExternal = ` +version: "3.8" + +services: + web: + image: nginx:latest + configs: + - source: web_config + target: /etc/nginx/nginx.conf + environment: + - NGINX_CONFIG=/etc/nginx/nginx.conf + + app: + image: node:14 + configs: + - source: app_config + target: /usr/src/app/config.json + + db: + image: postgres:13 + configs: + - source: db_config + target: /etc/postgresql/postgresql.conf + +configs: + web_config: + external: true + + app_config: + file: ./app-config.json + + db_config: + environment: dev + file: ./db-config.yml +`; + +const expectedComposeFileWithEnvAndExternal = load(` +version: "3.8" + +services: + web: + image: nginx:latest + configs: + - source: web_config-testhash + target: /etc/nginx/nginx.conf + environment: + - NGINX_CONFIG=/etc/nginx/nginx.conf + + app: + image: node:14 + configs: + - source: app_config-testhash + target: /usr/src/app/config.json + + db: + image: postgres:13 + configs: + - source: db_config-testhash + target: /etc/postgresql/postgresql.conf + +configs: + web_config-testhash: + external: true + + app_config-testhash: + file: ./app-config.json + + db_config-testhash: + environment: dev + file: ./db-config.yml +`) as ComposeSpecification; + +test("Add prefix to configs with environment and external", () => { + const composeData = load( + composeFileWithEnvAndExternal, + ) as ComposeSpecification; + + const prefix = "testhash"; + + const updatedComposeData = addPrefixToAllConfigs(composeData, prefix); + + expect(updatedComposeData).toEqual(expectedComposeFileWithEnvAndExternal); +}); + +const composeFileWithTemplateDriverAndLabels = ` +version: "3.8" + +services: + web: + image: nginx:latest + configs: + - source: web_config + target: /etc/nginx/nginx.conf + + app: + image: node:14 + configs: + - source: app_config + target: /usr/src/app/config.json + +configs: + web_config: + file: ./web-config.yml + template_driver: golang + + app_config: + file: ./app-config.json + labels: + - app=frontend + + db_config: + file: ./db-config.yml +`; + +const expectedComposeFileWithTemplateDriverAndLabels = load(` +version: "3.8" + +services: + web: + image: nginx:latest + configs: + - source: web_config-testhash + target: /etc/nginx/nginx.conf + + app: + image: node:14 + configs: + - source: app_config-testhash + target: /usr/src/app/config.json + +configs: + web_config-testhash: + file: ./web-config.yml + template_driver: golang + + app_config-testhash: + file: ./app-config.json + labels: + - app=frontend + + db_config-testhash: + file: ./db-config.yml +`) as ComposeSpecification; + +test("Add prefix to configs with template driver and labels", () => { + const composeData = load( + composeFileWithTemplateDriverAndLabels, + ) as ComposeSpecification; + + const prefix = "testhash"; + + const updatedComposeData = addPrefixToAllConfigs(composeData, prefix); + + expect(updatedComposeData).toEqual( + expectedComposeFileWithTemplateDriverAndLabels, + ); +}); diff --git a/__test__/compose/network/network-root.test.ts b/__test__/compose/network/network-root.test.ts new file mode 100644 index 00000000..b4a0929c --- /dev/null +++ b/__test__/compose/network/network-root.test.ts @@ -0,0 +1,281 @@ +import { generateRandomHash } from "@/server/utils/docker/compose"; +import { addPrefixToNetworksRoot } from "@/server/utils/docker/compose/network"; +import type { ComposeSpecification } from "@/server/utils/docker/types"; +import { load } from "js-yaml"; +import { expect, test } from "vitest"; + +const composeFile = ` +version: "3.8" + +services: + web: + image: nginx:latest + networks: + - frontend + +networks: + frontend: + driver: bridge + driver_opts: + com.docker.network.driver.mtu: 1200 + + backend: + driver: bridge + attachable: true + + external_network: + external: true + +`; + +test("Generate random hash with 8 characters", () => { + const hash = generateRandomHash(); + + expect(hash).toBeDefined(); + expect(hash.length).toBe(8); +}); + +test("Add prefix to networks root property", () => { + const composeData = load(composeFile) as ComposeSpecification; + + const prefix = generateRandomHash(); + + if (!composeData?.networks) { + return; + } + const networks = addPrefixToNetworksRoot(composeData.networks, prefix); + + expect(networks).toBeDefined(); + for (const volumeKey of Object.keys(networks)) { + expect(volumeKey).toContain(`-${prefix}`); + } +}); + +const composeFile2 = ` +version: "3.8" + +services: + app: + image: myapp:latest + networks: + - app_net + +networks: + app_net: + driver: bridge + driver_opts: + com.docker.network.driver.mtu: 1500 + ipam: + driver: default + config: + - subnet: 172.20.0.0/16 + + database_net: + driver: overlay + attachable: true + + monitoring_net: + driver: bridge + internal: true +`; + +test("Add prefix to advanced networks root property (2 TRY)", () => { + const composeData = load(composeFile2) as ComposeSpecification; + + const prefix = generateRandomHash(); + + if (!composeData?.networks) { + return; + } + const networks = addPrefixToNetworksRoot(composeData.networks, prefix); + + expect(networks).toBeDefined(); + for (const networkKey of Object.keys(networks)) { + expect(networkKey).toContain(`-${prefix}`); + } +}); + +const composeFile3 = ` +version: "3.8" + +services: + web: + image: nginx:latest + networks: + - frontend + - backend + +networks: + frontend: + external: + name: my_external_network + + backend: + driver: bridge + labels: + - "com.example.description=Backend network" + - "com.example.environment=production" + + external_network: + external: true +`; + +test("Add prefix to networks with external properties", () => { + const composeData = load(composeFile3) as ComposeSpecification; + + const prefix = generateRandomHash(); + + if (!composeData?.networks) { + return; + } + const networks = addPrefixToNetworksRoot(composeData.networks, prefix); + + expect(networks).toBeDefined(); + for (const networkKey of Object.keys(networks)) { + expect(networkKey).toContain(`-${prefix}`); + } +}); + +const composeFile4 = ` +version: "3.8" + +services: + db: + image: postgres:13 + networks: + - db_net + +networks: + db_net: + driver: bridge + ipam: + config: + - subnet: 192.168.1.0/24 + - gateway: 192.168.1.1 + - aux_addresses: + host1: 192.168.1.2 + host2: 192.168.1.3 + + external_network: + external: true +`; + +test("Add prefix to networks with IPAM configurations", () => { + const composeData = load(composeFile4) as ComposeSpecification; + + const prefix = generateRandomHash(); + + if (!composeData?.networks) { + return; + } + const networks = addPrefixToNetworksRoot(composeData.networks, prefix); + + expect(networks).toBeDefined(); + for (const networkKey of Object.keys(networks)) { + expect(networkKey).toContain(`-${prefix}`); + } +}); + +const composeFile5 = ` +version: "3.8" + +services: + api: + image: myapi:latest + networks: + - api_net + +networks: + api_net: + driver: bridge + options: + com.docker.network.bridge.name: br0 + enable_ipv6: true + ipam: + driver: default + config: + - subnet: "2001:db8:1::/64" + - gateway: "2001:db8:1::1" + + external_network: + external: true +`; + +test("Add prefix to networks with custom options", () => { + const composeData = load(composeFile5) as ComposeSpecification; + + const prefix = generateRandomHash(); + + if (!composeData?.networks) { + return; + } + const networks = addPrefixToNetworksRoot(composeData.networks, prefix); + + expect(networks).toBeDefined(); + for (const networkKey of Object.keys(networks)) { + expect(networkKey).toContain(`-${prefix}`); + } +}); + +const composeFile6 = ` +version: "3.8" + +services: + web: + image: nginx:latest + networks: + - frontend + +networks: + frontend: + driver: bridge + driver_opts: + com.docker.network.driver.mtu: 1200 + + backend: + driver: bridge + attachable: true + + external_network: + external: true +`; + +// Expected compose file with static prefix `testhash` +const expectedComposeFile6 = ` +version: "3.8" + +services: + web: + image: nginx:latest + networks: + - frontend-testhash + +networks: + frontend-testhash: + driver: bridge + driver_opts: + com.docker.network.driver.mtu: 1200 + + backend-testhash: + driver: bridge + attachable: true + + external_network-testhash: + external: true +`; + +test("Add prefix to networks with static prefix", () => { + const composeData = load(composeFile6) as ComposeSpecification; + + const prefix = "testhash"; + + if (!composeData?.networks) { + return; + } + const networks = addPrefixToNetworksRoot(composeData.networks, prefix); + + const expectedComposeData = load( + expectedComposeFile6, + ) as ComposeSpecification; + expect(networks).toStrictEqual(expectedComposeData.networks); +}); diff --git a/__test__/compose/network/network-service.test.ts b/__test__/compose/network/network-service.test.ts new file mode 100644 index 00000000..92df7a73 --- /dev/null +++ b/__test__/compose/network/network-service.test.ts @@ -0,0 +1,181 @@ +import { generateRandomHash } from "@/server/utils/docker/compose"; +import { addPrefixToServiceNetworks } from "@/server/utils/docker/compose/network"; +import type { ComposeSpecification } from "@/server/utils/docker/types"; +import { load } from "js-yaml"; +import { expect, test } from "vitest"; + +const composeFile = ` +version: "3.8" + +services: + web: + image: nginx:latest + networks: + - frontend + - backend + + api: + image: myapi:latest + networks: + - backend +`; + +test("Add prefix to networks in services", () => { + const composeData = load(composeFile) as ComposeSpecification; + + const prefix = generateRandomHash(); + + if (!composeData?.services) { + return; + } + const services = addPrefixToServiceNetworks(composeData.services, prefix); + const actualComposeData = { ...composeData, services }; + + expect(actualComposeData?.services?.web?.networks).toContain( + `frontend-${prefix}`, + ); + + expect(actualComposeData?.services?.api?.networks).toContain( + `backend-${prefix}`, + ); + + const apiNetworks = actualComposeData?.services?.api?.networks; + + expect(apiNetworks).toBeDefined(); + expect(actualComposeData?.services?.api?.networks).toContain( + `backend-${prefix}`, + ); +}); + +// Caso 2: Objeto con aliases +const composeFile2 = ` +version: "3.8" + +services: + api: + image: myapi:latest + networks: + frontend: + aliases: + - api + +networks: + frontend: + driver: bridge +`; + +test("Add prefix to networks in services with aliases", () => { + const composeData = load(composeFile2) as ComposeSpecification; + + const prefix = generateRandomHash(); + + if (!composeData?.services) { + return; + } + const services = addPrefixToServiceNetworks(composeData.services, prefix); + const actualComposeData = { ...composeData, services }; + + expect(actualComposeData.services?.api?.networks).toHaveProperty( + `frontend-${prefix}`, + ); + + const networkConfig = + actualComposeData?.services?.api?.networks[`frontend-${prefix}`]; + expect(networkConfig).toBeDefined(); + expect(networkConfig?.aliases).toContain("api"); + + expect(actualComposeData.services?.api?.networks).not.toHaveProperty( + "frontend-ash", + ); +}); + +const composeFile3 = ` +version: "3.8" + +services: + redis: + image: redis:alpine + networks: + backend: + +networks: + backend: + driver: bridge +`; + +test("Add prefix to networks in services (Object with simple networks)", () => { + const composeData = load(composeFile3) as ComposeSpecification; + + const prefix = generateRandomHash(); + + if (!composeData?.services) { + return; + } + const services = addPrefixToServiceNetworks(composeData.services, prefix); + const actualComposeData = { ...composeData, services }; + + expect(actualComposeData.services?.redis?.networks).toHaveProperty( + `backend-${prefix}`, + ); +}); + +const composeFileCombined = ` +version: "3.8" + +services: + web: + image: nginx:latest + networks: + - frontend + - backend + + api: + image: myapi:latest + networks: + frontend: + aliases: + - api + + redis: + image: redis:alpine + networks: + backend: + +networks: + frontend: + driver: bridge + + backend: + driver: bridge +`; + +test("Add prefix to networks in services (combined case)", () => { + const composeData = load(composeFileCombined) as ComposeSpecification; + + const prefix = generateRandomHash(); + + if (!composeData?.services) { + return; + } + const services = addPrefixToServiceNetworks(composeData.services, prefix); + const actualComposeData = { ...composeData, services }; + + // Caso 1: ListOfStrings + expect(actualComposeData.services?.web?.networks).toContain( + `frontend-${prefix}`, + ); + expect(actualComposeData.services?.web?.networks).toContain( + `backend-${prefix}`, + ); + + // Caso 2: Objeto con aliases + const apiNetworks = actualComposeData.services?.api?.networks; + expect(apiNetworks).toHaveProperty(`frontend-${prefix}`); + expect(apiNetworks[`frontend-${prefix}`]).toBeDefined(); + expect(apiNetworks).not.toHaveProperty("frontend"); + + // Caso 3: Objeto con redes simples + const redisNetworks = actualComposeData.services?.redis?.networks; + expect(redisNetworks).toHaveProperty(`backend-${prefix}`); + expect(redisNetworks).not.toHaveProperty("backend"); +}); diff --git a/__test__/compose/network/network.test.ts b/__test__/compose/network/network.test.ts new file mode 100644 index 00000000..ae938775 --- /dev/null +++ b/__test__/compose/network/network.test.ts @@ -0,0 +1,254 @@ +import { generateRandomHash } from "@/server/utils/docker/compose"; +import { + addPrefixToAllNetworks, + addPrefixToServiceNetworks, +} from "@/server/utils/docker/compose/network"; +import { addPrefixToNetworksRoot } from "@/server/utils/docker/compose/network"; +import type { ComposeSpecification } from "@/server/utils/docker/types"; +import { load } from "js-yaml"; +import { expect, test } from "vitest"; + +const composeFileCombined = ` +version: "3.8" + +services: + web: + image: nginx:latest + networks: + - frontend + - backend + + api: + image: myapi:latest + networks: + frontend: + aliases: + - api + + redis: + image: redis:alpine + networks: + backend: + +networks: + frontend: + driver: bridge + + backend: + driver: bridge +`; + +test("Add prefix to networks in services and root (combined case)", () => { + const composeData = load(composeFileCombined) as ComposeSpecification; + + const prefix = generateRandomHash(); + + // Prefijo para redes definidas en el root + if (composeData.networks) { + composeData.networks = addPrefixToNetworksRoot( + composeData.networks, + prefix, + ); + } + + // Prefijo para redes definidas en los servicios + if (composeData.services) { + composeData.services = addPrefixToServiceNetworks( + composeData.services, + prefix, + ); + } + + const actualComposeData = { ...composeData }; + + // Verificar redes en root + expect(actualComposeData.networks).toHaveProperty(`frontend-${prefix}`); + expect(actualComposeData.networks).toHaveProperty(`backend-${prefix}`); + expect(actualComposeData.networks).not.toHaveProperty("frontend"); + expect(actualComposeData.networks).not.toHaveProperty("backend"); + + // Caso 1: ListOfStrings + expect(actualComposeData.services?.web?.networks).toContain( + `frontend-${prefix}`, + ); + expect(actualComposeData.services?.web?.networks).toContain( + `backend-${prefix}`, + ); + + // Caso 2: Objeto con aliases + const apiNetworks = actualComposeData.services?.api?.networks; + expect(apiNetworks).toHaveProperty(`frontend-${prefix}`); + expect(apiNetworks[`frontend-${prefix}`]?.aliases).toContain("api"); + expect(apiNetworks).not.toHaveProperty("frontend"); + + // Caso 3: Objeto con redes simples + const redisNetworks = actualComposeData.services?.redis?.networks; + expect(redisNetworks).toHaveProperty(`backend-${prefix}`); + expect(redisNetworks).not.toHaveProperty("backend"); +}); + +const expectedComposeFile = load(` +version: "3.8" + +services: + web: + image: nginx:latest + networks: + - frontend-testhash + - backend-testhash + + api: + image: myapi:latest + networks: + frontend-testhash: + aliases: + - api + + redis: + image: redis:alpine + networks: + backend-testhash: + +networks: + frontend-testhash: + driver: bridge + + backend-testhash: + driver: bridge +`); + +test("Add prefix to networks in compose file", () => { + const composeData = load(composeFileCombined) as ComposeSpecification; + + const prefix = "testhash"; + if (!composeData?.networks) { + return; + } + const updatedComposeData = addPrefixToAllNetworks(composeData, prefix); + + expect(updatedComposeData).toEqual(expectedComposeFile); +}); + +const composeFile2 = ` +version: "3.8" + +services: + web: + image: nginx:latest + networks: + - frontend + - backend + + db: + image: postgres:latest + networks: + backend: + aliases: + - db + +networks: + frontend: + external: true + + backend: + driver: bridge +`; + +const expectedComposeFile2 = load(` +version: "3.8" + +services: + web: + image: nginx:latest + networks: + - frontend-testhash + - backend-testhash + + db: + image: postgres:latest + networks: + backend-testhash: + aliases: + - db + +networks: + frontend-testhash: + external: true + + backend-testhash: + driver: bridge +`); + +test("Add prefix to networks in compose file with external and internal networks", () => { + const composeData = load(composeFile2) as ComposeSpecification; + + const prefix = "testhash"; + const updatedComposeData = addPrefixToAllNetworks(composeData, prefix); + + expect(updatedComposeData).toEqual(expectedComposeFile2); +}); + +const composeFile3 = ` +version: "3.8" + +services: + app: + image: myapp:latest + networks: + frontend: + aliases: + - app + backend: + + worker: + image: worker:latest + networks: + - backend + +networks: + frontend: + driver: bridge + attachable: true + + backend: + driver: bridge + driver_opts: + com.docker.network.bridge.enable_icc: "true" +`; + +const expectedComposeFile3 = load(` +version: "3.8" + +services: + app: + image: myapp:latest + networks: + frontend-testhash: + aliases: + - app + backend-testhash: + + worker: + image: worker:latest + networks: + - backend-testhash + +networks: + frontend-testhash: + driver: bridge + attachable: true + + backend-testhash: + driver: bridge + driver_opts: + com.docker.network.bridge.enable_icc: "true" +`); + +test("Add prefix to networks in compose file with multiple services and complex network configurations", () => { + const composeData = load(composeFile3) as ComposeSpecification; + + const prefix = "testhash"; + const updatedComposeData = addPrefixToAllNetworks(composeData, prefix); + + expect(updatedComposeData).toEqual(expectedComposeFile3); +}); diff --git a/__test__/compose/secrets/secret-root.test.ts b/__test__/compose/secrets/secret-root.test.ts new file mode 100644 index 00000000..861343a2 --- /dev/null +++ b/__test__/compose/secrets/secret-root.test.ts @@ -0,0 +1,103 @@ +import { expect, test } from "vitest"; +import { load, dump } from "js-yaml"; +import { generateRandomHash } from "@/server/utils/docker/compose"; +import type { ComposeSpecification } from "@/server/utils/docker/types"; +import { addPrefixToSecretsRoot } from "@/server/utils/docker/compose/secrets"; + +test("Generate random hash with 8 characters", () => { + const hash = generateRandomHash(); + + expect(hash).toBeDefined(); + expect(hash.length).toBe(8); +}); + +const composeFileSecretsRoot = ` +version: "3.8" + +services: + web: + image: nginx:latest + +secrets: + db_password: + file: ./db_password.txt +`; + +test("Add prefix to secrets in root property", () => { + const composeData = load(composeFileSecretsRoot) as ComposeSpecification; + const prefix = generateRandomHash(); + + if (!composeData?.secrets) { + return; + } + const secrets = addPrefixToSecretsRoot(composeData.secrets, prefix); + expect(secrets).toBeDefined(); + if (secrets) { + for (const secretKey of Object.keys(secrets)) { + expect(secretKey).toContain(`-${prefix}`); + expect(secrets[secretKey]).toBeDefined(); + } + } +}); + +const composeFileSecretsRoot1 = ` +version: "3.8" + +services: + api: + image: myapi:latest + +secrets: + api_key: + file: ./api_key.txt +`; + +test("Add prefix to secrets in root property (Test 1)", () => { + const composeData = load(composeFileSecretsRoot1) as ComposeSpecification; + const prefix = generateRandomHash(); + + if (!composeData?.secrets) { + return; + } + const secrets = addPrefixToSecretsRoot(composeData.secrets, prefix); + expect(secrets).toBeDefined(); + + if (secrets) { + for (const secretKey of Object.keys(secrets)) { + expect(secretKey).toContain(`-${prefix}`); + expect(secrets[secretKey]).toBeDefined(); + } + } +}); + +const composeFileSecretsRoot2 = ` +version: "3.8" + +services: + frontend: + image: nginx:latest + +secrets: + frontend_secret: + file: ./frontend_secret.txt + db_password: + external: true +`; + +test("Add prefix to secrets in root property (Test 2)", () => { + const composeData = load(composeFileSecretsRoot2) as ComposeSpecification; + const prefix = generateRandomHash(); + + if (!composeData?.secrets) { + return; + } + const secrets = addPrefixToSecretsRoot(composeData.secrets, prefix); + expect(secrets).toBeDefined(); + + if (secrets) { + for (const secretKey of Object.keys(secrets)) { + expect(secretKey).toContain(`-${prefix}`); + expect(secrets[secretKey]).toBeDefined(); + } + } +}); diff --git a/__test__/compose/secrets/secret-services.test.ts b/__test__/compose/secrets/secret-services.test.ts new file mode 100644 index 00000000..1a58314d --- /dev/null +++ b/__test__/compose/secrets/secret-services.test.ts @@ -0,0 +1,113 @@ +import { generateRandomHash } from "@/server/utils/docker/compose"; +import { addPrefixToSecretsInServices } from "@/server/utils/docker/compose/secrets"; +import type { ComposeSpecification } from "@/server/utils/docker/types"; +import { load } from "js-yaml"; +import { expect, test } from "vitest"; + +const composeFileSecretsServices = ` +version: "3.8" + +services: + db: + image: postgres:latest + secrets: + - db_password + +secrets: + db_password: + file: ./db_password.txt +`; + +test("Add prefix to secrets in services", () => { + const composeData = load(composeFileSecretsServices) as ComposeSpecification; + const prefix = generateRandomHash(); + + if (!composeData.services) { + return; + } + + const updatedComposeData = addPrefixToSecretsInServices( + composeData.services, + prefix, + ); + const actualComposeData = { ...composeData, services: updatedComposeData }; + + expect(actualComposeData.services?.db?.secrets).toContain( + `db_password-${prefix}`, + ); +}); + +const composeFileSecretsServices1 = ` +version: "3.8" + +services: + app: + image: node:14 + secrets: + - app_secret + +secrets: + app_secret: + file: ./app_secret.txt +`; + +test("Add prefix to secrets in services (Test 1)", () => { + const composeData = load(composeFileSecretsServices1) as ComposeSpecification; + const prefix = generateRandomHash(); + + if (!composeData.services) { + return; + } + + const updatedComposeData = addPrefixToSecretsInServices( + composeData.services, + prefix, + ); + const actualComposeData = { ...composeData, services: updatedComposeData }; + + expect(actualComposeData.services?.app?.secrets).toContain( + `app_secret-${prefix}`, + ); +}); + +const composeFileSecretsServices2 = ` +version: "3.8" + +services: + backend: + image: backend:latest + secrets: + - backend_secret + frontend: + image: frontend:latest + secrets: + - frontend_secret + +secrets: + backend_secret: + file: ./backend_secret.txt + frontend_secret: + file: ./frontend_secret.txt +`; + +test("Add prefix to secrets in services (Test 2)", () => { + const composeData = load(composeFileSecretsServices2) as ComposeSpecification; + const prefix = generateRandomHash(); + + if (!composeData.services) { + return; + } + + const updatedComposeData = addPrefixToSecretsInServices( + composeData.services, + prefix, + ); + const actualComposeData = { ...composeData, services: updatedComposeData }; + + expect(actualComposeData.services?.backend?.secrets).toContain( + `backend_secret-${prefix}`, + ); + expect(actualComposeData.services?.frontend?.secrets).toContain( + `frontend_secret-${prefix}`, + ); +}); diff --git a/__test__/compose/secrets/secret.test.ts b/__test__/compose/secrets/secret.test.ts new file mode 100644 index 00000000..7922eb20 --- /dev/null +++ b/__test__/compose/secrets/secret.test.ts @@ -0,0 +1,159 @@ +import { addPrefixToAllSecrets } from "@/server/utils/docker/compose/secrets"; +import type { ComposeSpecification } from "@/server/utils/docker/types"; +import { load } from "js-yaml"; +import { expect, test } from "vitest"; + +const composeFileCombinedSecrets = ` +version: "3.8" + +services: + web: + image: nginx:latest + secrets: + - web_secret + + app: + image: node:14 + secrets: + - app_secret + +secrets: + web_secret: + file: ./web_secret.txt + + app_secret: + file: ./app_secret.txt +`; + +const expectedComposeFileCombinedSecrets = load(` +version: "3.8" + +services: + web: + image: nginx:latest + secrets: + - web_secret-testhash + + app: + image: node:14 + secrets: + - app_secret-testhash + +secrets: + web_secret-testhash: + file: ./web_secret.txt + + app_secret-testhash: + file: ./app_secret.txt +`) as ComposeSpecification; + +test("Add prefix to all secrets", () => { + const composeData = load(composeFileCombinedSecrets) as ComposeSpecification; + const prefix = "testhash"; + + const updatedComposeData = addPrefixToAllSecrets(composeData, prefix); + + expect(updatedComposeData).toEqual(expectedComposeFileCombinedSecrets); +}); + +const composeFileCombinedSecrets3 = ` +version: "3.8" + +services: + api: + image: myapi:latest + secrets: + - api_key + + cache: + image: redis:latest + secrets: + - cache_secret + +secrets: + api_key: + file: ./api_key.txt + cache_secret: + file: ./cache_secret.txt +`; + +const expectedComposeFileCombinedSecrets3 = load(` +version: "3.8" + +services: + api: + image: myapi:latest + secrets: + - api_key-testhash + + cache: + image: redis:latest + secrets: + - cache_secret-testhash + +secrets: + api_key-testhash: + file: ./api_key.txt + cache_secret-testhash: + file: ./cache_secret.txt +`) as ComposeSpecification; + +test("Add prefix to all secrets (3rd Case)", () => { + const composeData = load(composeFileCombinedSecrets3) as ComposeSpecification; + const prefix = "testhash"; + + const updatedComposeData = addPrefixToAllSecrets(composeData, prefix); + + expect(updatedComposeData).toEqual(expectedComposeFileCombinedSecrets3); +}); + +const composeFileCombinedSecrets4 = ` +version: "3.8" + +services: + web: + image: nginx:latest + secrets: + - web_secret + + db: + image: postgres:latest + secrets: + - db_password + +secrets: + web_secret: + file: ./web_secret.txt + db_password: + file: ./db_password.txt +`; + +const expectedComposeFileCombinedSecrets4 = load(` +version: "3.8" + +services: + web: + image: nginx:latest + secrets: + - web_secret-testhash + + db: + image: postgres:latest + secrets: + - db_password-testhash + +secrets: + web_secret-testhash: + file: ./web_secret.txt + db_password-testhash: + file: ./db_password.txt +`) as ComposeSpecification; + +test("Add prefix to all secrets (4th Case)", () => { + const composeData = load(composeFileCombinedSecrets4) as ComposeSpecification; + const prefix = "testhash"; + + const updatedComposeData = addPrefixToAllSecrets(composeData, prefix); + + expect(updatedComposeData).toEqual(expectedComposeFileCombinedSecrets4); +}); diff --git a/__test__/compose/service/service-container-name.test.ts b/__test__/compose/service/service-container-name.test.ts new file mode 100644 index 00000000..9e3bfa80 --- /dev/null +++ b/__test__/compose/service/service-container-name.test.ts @@ -0,0 +1,59 @@ +import { generateRandomHash } from "@/server/utils/docker/compose"; +import { addPrefixToServiceNames } from "@/server/utils/docker/compose/service"; +import type { ComposeSpecification } from "@/server/utils/docker/types"; +import { load } from "js-yaml"; +import { expect, test } from "vitest"; + +const composeFile = ` +version: "3.8" + +services: + web: + image: nginx:latest + container_name: web_container + + api: + image: myapi:latest + +networks: + default: + driver: bridge +`; + +test("Generate random hash with 8 characters", () => { + const hash = generateRandomHash(); + + expect(hash).toBeDefined(); + expect(hash.length).toBe(8); +}); + +test("Add prefix to service names with container_name in compose file", () => { + const composeData = load(composeFile) as ComposeSpecification; + + const prefix = generateRandomHash(); + + if (!composeData.services) { + return; + } + const updatedComposeData = addPrefixToServiceNames( + composeData.services, + prefix, + ); + const actualComposeData = { ...composeData, services: updatedComposeData }; + + // Verificar que el nombre del contenedor ha cambiado correctamente + expect(actualComposeData.services[`web-${prefix}`].container_name).toBe( + `web_container-${prefix}`, + ); + // Verificar que la nueva clave del servicio tiene el prefijo y la vieja clave no existe + expect(actualComposeData.services).toHaveProperty(`web-${prefix}`); + expect(actualComposeData.services).not.toHaveProperty("web"); + + // Verificar que la configuración de la imagen sigue igual + expect(actualComposeData.services[`web-${prefix}`].image).toBe( + "nginx:latest", + ); + expect(actualComposeData.services[`api-${prefix}`].image).toBe( + "myapi:latest", + ); +}); diff --git a/__test__/compose/service/service-depends-on.test.ts b/__test__/compose/service/service-depends-on.test.ts new file mode 100644 index 00000000..cf4ca132 --- /dev/null +++ b/__test__/compose/service/service-depends-on.test.ts @@ -0,0 +1,150 @@ +import { generateRandomHash } from "@/server/utils/docker/compose"; +import { addPrefixToServiceNames } from "@/server/utils/docker/compose/service"; +import type { ComposeSpecification } from "@/server/utils/docker/types"; +import { load } from "js-yaml"; +import { expect, test } from "vitest"; + +test("Generate random hash with 8 characters", () => { + const hash = generateRandomHash(); + + expect(hash).toBeDefined(); + expect(hash.length).toBe(8); +}); + +const composeFile4 = ` +version: "3.8" + +services: + web: + image: nginx:latest + depends_on: + - db + - api + + api: + image: myapi:latest + + db: + image: postgres:latest + +networks: + default: + driver: bridge +`; + +test("Add prefix to service names with depends_on (array) in compose file", () => { + const composeData = load(composeFile4) as ComposeSpecification; + + const prefix = generateRandomHash(); + + if (!composeData.services) { + return; + } + const updatedComposeData = addPrefixToServiceNames( + composeData.services, + prefix, + ); + const actualComposeData = { ...composeData, services: updatedComposeData }; + + // Verificar que la nueva clave del servicio tiene el prefijo y la vieja clave no existe + expect(actualComposeData.services).toHaveProperty(`web-${prefix}`); + expect(actualComposeData.services).not.toHaveProperty("web"); + + // Verificar que la configuración de la imagen sigue igual + expect(actualComposeData.services[`web-${prefix}`].image).toBe( + "nginx:latest", + ); + expect(actualComposeData.services[`api-${prefix}`].image).toBe( + "myapi:latest", + ); + + // Verificar que los nombres en depends_on tienen el prefijo + expect(actualComposeData.services[`web-${prefix}`].depends_on).toContain( + `db-${prefix}`, + ); + expect(actualComposeData.services[`web-${prefix}`].depends_on).toContain( + `api-${prefix}`, + ); + + // Verificar que los servicios `db` y `api` también tienen el prefijo + expect(actualComposeData.services).toHaveProperty(`db-${prefix}`); + expect(actualComposeData.services).not.toHaveProperty("db"); + expect(actualComposeData.services[`db-${prefix}`].image).toBe( + "postgres:latest", + ); + expect(actualComposeData.services).toHaveProperty(`api-${prefix}`); + expect(actualComposeData.services).not.toHaveProperty("api"); + expect(actualComposeData.services[`api-${prefix}`].image).toBe( + "myapi:latest", + ); +}); + +const composeFile5 = ` +version: "3.8" + +services: + web: + image: nginx:latest + depends_on: + db: + condition: service_healthy + api: + condition: service_started + + api: + image: myapi:latest + + db: + image: postgres:latest + +networks: + default: + driver: bridge +`; + +test("Add prefix to service names with depends_on (object) in compose file", () => { + const composeData = load(composeFile5) as ComposeSpecification; + + const prefix = generateRandomHash(); + + if (!composeData.services) { + return; + } + const updatedComposeData = addPrefixToServiceNames( + composeData.services, + prefix, + ); + const actualComposeData = { ...composeData, services: updatedComposeData }; + + // Verificar que la nueva clave del servicio tiene el prefijo y la vieja clave no existe + expect(actualComposeData.services).toHaveProperty(`web-${prefix}`); + expect(actualComposeData.services).not.toHaveProperty("web"); + + // Verificar que la configuración de la imagen sigue igual + expect(actualComposeData.services[`web-${prefix}`].image).toBe( + "nginx:latest", + ); + expect(actualComposeData.services[`api-${prefix}`].image).toBe( + "myapi:latest", + ); + + // Verificar que los nombres en depends_on tienen el prefijo + const webDependsOn = actualComposeData.services[`web-${prefix}`] + .depends_on as Record; + expect(webDependsOn).toHaveProperty(`db-${prefix}`); + expect(webDependsOn).toHaveProperty(`api-${prefix}`); + expect(webDependsOn[`db-${prefix}`].condition).toBe("service_healthy"); + expect(webDependsOn[`api-${prefix}`].condition).toBe("service_started"); + + // Verificar que los servicios `db` y `api` también tienen el prefijo + expect(actualComposeData.services).toHaveProperty(`db-${prefix}`); + expect(actualComposeData.services).not.toHaveProperty("db"); + expect(actualComposeData.services[`db-${prefix}`].image).toBe( + "postgres:latest", + ); + expect(actualComposeData.services).toHaveProperty(`api-${prefix}`); + expect(actualComposeData.services).not.toHaveProperty("api"); + expect(actualComposeData.services[`api-${prefix}`].image).toBe( + "myapi:latest", + ); +}); diff --git a/__test__/compose/service/service-extends.test.ts b/__test__/compose/service/service-extends.test.ts new file mode 100644 index 00000000..6188e4a8 --- /dev/null +++ b/__test__/compose/service/service-extends.test.ts @@ -0,0 +1,131 @@ +import { generateRandomHash } from "@/server/utils/docker/compose"; +import { addPrefixToServiceNames } from "@/server/utils/docker/compose/service"; +import type { ComposeSpecification } from "@/server/utils/docker/types"; +import { load } from "js-yaml"; +import { expect, test } from "vitest"; + +test("Generate random hash with 8 characters", () => { + const hash = generateRandomHash(); + + expect(hash).toBeDefined(); + expect(hash.length).toBe(8); +}); + +const composeFile6 = ` +version: "3.8" + +services: + web: + image: nginx:latest + extends: base_service + + api: + image: myapi:latest + + base_service: + image: base:latest + +networks: + default: + driver: bridge +`; + +test("Add prefix to service names with extends (string) in compose file", () => { + const composeData = load(composeFile6) as ComposeSpecification; + + const prefix = generateRandomHash(); + + if (!composeData.services) { + return; + } + const updatedComposeData = addPrefixToServiceNames( + composeData.services, + prefix, + ); + const actualComposeData = { ...composeData, services: updatedComposeData }; + + // Verificar que la nueva clave del servicio tiene el prefijo y la vieja clave no existe + expect(actualComposeData.services).toHaveProperty(`web-${prefix}`); + expect(actualComposeData.services).not.toHaveProperty("web"); + + // Verificar que la configuración de la imagen sigue igual + expect(actualComposeData.services[`web-${prefix}`].image).toBe( + "nginx:latest", + ); + expect(actualComposeData.services[`api-${prefix}`].image).toBe( + "myapi:latest", + ); + + // Verificar que el nombre en extends tiene el prefijo + expect(actualComposeData.services[`web-${prefix}`].extends).toBe( + `base_service-${prefix}`, + ); + + // Verificar que el servicio `base_service` también tiene el prefijo + expect(actualComposeData.services).toHaveProperty(`base_service-${prefix}`); + expect(actualComposeData.services).not.toHaveProperty("base_service"); + expect(actualComposeData.services[`base_service-${prefix}`].image).toBe( + "base:latest", + ); +}); + +const composeFile7 = ` +version: "3.8" + +services: + web: + image: nginx:latest + extends: + service: base_service + file: docker-compose.base.yml + + api: + image: myapi:latest + + base_service: + image: base:latest + +networks: + default: + driver: bridge +`; + +test("Add prefix to service names with extends (object) in compose file", () => { + const composeData = load(composeFile7) as ComposeSpecification; + + const prefix = generateRandomHash(); + + if (!composeData.services) { + return; + } + const updatedComposeData = addPrefixToServiceNames( + composeData.services, + prefix, + ); + const actualComposeData = { ...composeData, services: updatedComposeData }; + + // Verificar que la nueva clave del servicio tiene el prefijo y la vieja clave no existe + expect(actualComposeData.services).toHaveProperty(`web-${prefix}`); + expect(actualComposeData.services).not.toHaveProperty("web"); + + // Verificar que la configuración de la imagen sigue igual + expect(actualComposeData.services[`web-${prefix}`].image).toBe( + "nginx:latest", + ); + expect(actualComposeData.services[`api-${prefix}`].image).toBe( + "myapi:latest", + ); + + // Verificar que el nombre en extends.service tiene el prefijo + const webExtends = actualComposeData.services[`web-${prefix}`].extends; + if (typeof webExtends !== "string") { + expect(webExtends.service).toBe(`base_service-${prefix}`); + } + + // Verificar que el servicio `base_service` también tiene el prefijo + expect(actualComposeData.services).toHaveProperty(`base_service-${prefix}`); + expect(actualComposeData.services).not.toHaveProperty("base_service"); + expect(actualComposeData.services[`base_service-${prefix}`].image).toBe( + "base:latest", + ); +}); diff --git a/__test__/compose/service/service-links.test.ts b/__test__/compose/service/service-links.test.ts new file mode 100644 index 00000000..b9b22fdf --- /dev/null +++ b/__test__/compose/service/service-links.test.ts @@ -0,0 +1,76 @@ +import { generateRandomHash } from "@/server/utils/docker/compose"; +import { addPrefixToServiceNames } from "@/server/utils/docker/compose/service"; +import type { ComposeSpecification } from "@/server/utils/docker/types"; +import { load } from "js-yaml"; +import { expect, test } from "vitest"; + +test("Generate random hash with 8 characters", () => { + const hash = generateRandomHash(); + + expect(hash).toBeDefined(); + expect(hash.length).toBe(8); +}); + +const composeFile2 = ` +version: "3.8" + +services: + web: + image: nginx:latest + links: + - db + + api: + image: myapi:latest + + db: + image: postgres:latest + +networks: + default: + driver: bridge +`; + +test("Add prefix to service names with links in compose file", () => { + const composeData = load(composeFile2) as ComposeSpecification; + + const prefix = generateRandomHash(); + + if (!composeData.services) { + return; + } + const updatedComposeData = addPrefixToServiceNames( + composeData.services, + prefix, + ); + const actualComposeData = { ...composeData, services: updatedComposeData }; + + // Verificar que la nueva clave del servicio tiene el prefijo y la vieja clave no existe + expect(actualComposeData.services).toHaveProperty(`web-${prefix}`); + expect(actualComposeData.services).not.toHaveProperty("web"); + + // Verificar que la configuración de la imagen sigue igual + expect(actualComposeData.services[`web-${prefix}`].image).toBe( + "nginx:latest", + ); + expect(actualComposeData.services[`api-${prefix}`].image).toBe( + "myapi:latest", + ); + + // Verificar que los nombres en links tienen el prefijo + expect(actualComposeData.services[`web-${prefix}`].links).toContain( + `db-${prefix}`, + ); + + // Verificar que los servicios `db` y `api` también tienen el prefijo + expect(actualComposeData.services).toHaveProperty(`db-${prefix}`); + expect(actualComposeData.services).not.toHaveProperty("db"); + expect(actualComposeData.services[`db-${prefix}`].image).toBe( + "postgres:latest", + ); + expect(actualComposeData.services).toHaveProperty(`api-${prefix}`); + expect(actualComposeData.services).not.toHaveProperty("api"); + expect(actualComposeData.services[`api-${prefix}`].image).toBe( + "myapi:latest", + ); +}); diff --git a/__test__/compose/service/service-names.test.ts b/__test__/compose/service/service-names.test.ts new file mode 100644 index 00000000..0f071b2e --- /dev/null +++ b/__test__/compose/service/service-names.test.ts @@ -0,0 +1,49 @@ +import { generateRandomHash } from "@/server/utils/docker/compose"; +import { addPrefixToServiceNames } from "@/server/utils/docker/compose/service"; +import type { ComposeSpecification } from "@/server/utils/docker/types"; +import { load } from "js-yaml"; +import { expect, test } from "vitest"; + +test("Generate random hash with 8 characters", () => { + const hash = generateRandomHash(); + + expect(hash).toBeDefined(); + expect(hash.length).toBe(8); +}); + +const composeFile = ` +version: "3.8" + +services: + web: + image: nginx:latest + + api: + image: myapi:latest + +networks: + default: + driver: bridge +`; + +test("Add prefix to service names in compose file", () => { + const composeData = load(composeFile) as ComposeSpecification; + + const prefix = generateRandomHash(); + + if (!composeData.services) { + return; + } + const updatedComposeData = addPrefixToServiceNames( + composeData.services, + prefix, + ); + const actualComposeData = { ...composeData, services: updatedComposeData }; + + // Verificar que los nombres de los servicios han cambiado correctamente + expect(actualComposeData.services).toHaveProperty(`web-${prefix}`); + expect(actualComposeData.services).toHaveProperty(`api-${prefix}`); + // Verificar que las claves originales no existen + expect(actualComposeData.services).not.toHaveProperty("web"); + expect(actualComposeData.services).not.toHaveProperty("api"); +}); diff --git a/__test__/compose/service/service.test.ts b/__test__/compose/service/service.test.ts new file mode 100644 index 00000000..113576ae --- /dev/null +++ b/__test__/compose/service/service.test.ts @@ -0,0 +1,375 @@ +import { + addPrefixToAllServiceNames, + addPrefixToServiceNames, +} from "@/server/utils/docker/compose/service"; +import type { ComposeSpecification } from "@/server/utils/docker/types"; +import { load } from "js-yaml"; +import { expect, test } from "vitest"; + +const composeFileCombinedAllCases = ` +version: "3.8" + +services: + web: + image: nginx:latest + container_name: web_container + links: + - api + depends_on: + - api + extends: base_service + + api: + image: myapi:latest + depends_on: + db: + condition: service_healthy + volumes_from: + - db + + db: + image: postgres:latest + + base_service: + image: base:latest + +networks: + default: + driver: bridge +`; + +const expectedComposeFile = load(` +version: "3.8" + +services: + web-testhash: + image: nginx:latest + container_name: web_container-testhash + links: + - api-testhash + depends_on: + - api-testhash + extends: base_service-testhash + + api-testhash: + image: myapi:latest + depends_on: + db-testhash: + condition: service_healthy + volumes_from: + - db-testhash + + db-testhash: + image: postgres:latest + + base_service-testhash: + image: base:latest + +networks: + default: + driver: bridge +`); + +test("Add prefix to all service names in compose file", () => { + const composeData = load(composeFileCombinedAllCases) as ComposeSpecification; + + const prefix = "testhash"; + + if (!composeData.services) { + return; + } + const updatedComposeData = addPrefixToServiceNames( + composeData.services, + prefix, + ); + const actualComposeData = { ...composeData, services: updatedComposeData }; + + expect(actualComposeData).toEqual(expectedComposeFile); +}); + +const composeFile1 = ` +version: "3.8" + +services: + web: + image: nginx:latest + container_name: web_container + depends_on: + - app + networks: + - frontend + volumes_from: + - data + links: + - db + extends: + service: base_service + + app: + image: node:14 + networks: + - backend + - frontend + + db: + image: postgres:13 + networks: + - backend + + data: + image: busybox + volumes: + - /data + + base_service: + image: base:latest + +networks: + frontend: + driver: bridge + backend: + driver: bridge +`; + +const expectedComposeFile1 = load(` +version: "3.8" + +services: + web-testhash: + image: nginx:latest + container_name: web_container-testhash + depends_on: + - app-testhash + networks: + - frontend + volumes_from: + - data-testhash + links: + - db-testhash + extends: + service: base_service-testhash + + app-testhash: + image: node:14 + networks: + - backend + - frontend + + db-testhash: + image: postgres:13 + networks: + - backend + + data-testhash: + image: busybox + volumes: + - /data + + base_service-testhash: + image: base:latest + +networks: + frontend: + driver: bridge + backend: + driver: bridge +`) as ComposeSpecification; + +test("Add prefix to all service names in compose file 1", () => { + const composeData = load(composeFile1) as ComposeSpecification; + const prefix = "testhash"; + + const updatedComposeData = addPrefixToAllServiceNames(composeData, prefix); + + expect(updatedComposeData).toEqual(expectedComposeFile1); +}); + +const composeFile2 = ` +version: "3.8" + +services: + frontend: + image: nginx:latest + depends_on: + - backend + networks: + - public + volumes_from: + - logs + links: + - cache + extends: + service: shared_service + + backend: + image: node:14 + networks: + - private + - public + + cache: + image: redis:latest + networks: + - private + + logs: + image: busybox + volumes: + - /logs + + shared_service: + image: shared:latest + +networks: + public: + driver: bridge + private: + driver: bridge +`; + +const expectedComposeFile2 = load(` +version: "3.8" + +services: + frontend-testhash: + image: nginx:latest + depends_on: + - backend-testhash + networks: + - public + volumes_from: + - logs-testhash + links: + - cache-testhash + extends: + service: shared_service-testhash + + backend-testhash: + image: node:14 + networks: + - private + - public + + cache-testhash: + image: redis:latest + networks: + - private + + logs-testhash: + image: busybox + volumes: + - /logs + + shared_service-testhash: + image: shared:latest + +networks: + public: + driver: bridge + private: + driver: bridge +`) as ComposeSpecification; + +test("Add prefix to all service names in compose file 2", () => { + const composeData = load(composeFile2) as ComposeSpecification; + const prefix = "testhash"; + + const updatedComposeData = addPrefixToAllServiceNames(composeData, prefix); + + expect(updatedComposeData).toEqual(expectedComposeFile2); +}); + +const composeFile3 = ` +version: "3.8" + +services: + service_a: + image: service_a:latest + depends_on: + - service_b + networks: + - net_a + volumes_from: + - data_volume + links: + - service_c + extends: + service: common_service + + service_b: + image: service_b:latest + networks: + - net_b + - net_a + + service_c: + image: service_c:latest + networks: + - net_b + + data_volume: + image: busybox + volumes: + - /data + + common_service: + image: common:latest + +networks: + net_a: + driver: bridge + net_b: + driver: bridge +`; + +const expectedComposeFile3 = load(` +version: "3.8" + +services: + service_a-testhash: + image: service_a:latest + depends_on: + - service_b-testhash + networks: + - net_a + volumes_from: + - data_volume-testhash + links: + - service_c-testhash + extends: + service: common_service-testhash + + service_b-testhash: + image: service_b:latest + networks: + - net_b + - net_a + + service_c-testhash: + image: service_c:latest + networks: + - net_b + + data_volume-testhash: + image: busybox + volumes: + - /data + + common_service-testhash: + image: common:latest + +networks: + net_a: + driver: bridge + net_b: + driver: bridge +`) as ComposeSpecification; + +test("Add prefix to all service names in compose file 3", () => { + const composeData = load(composeFile3) as ComposeSpecification; + const prefix = "testhash"; + + const updatedComposeData = addPrefixToAllServiceNames(composeData, prefix); + + expect(updatedComposeData).toEqual(expectedComposeFile3); +}); diff --git a/__test__/compose/service/sevice-volumes-from.test.ts b/__test__/compose/service/sevice-volumes-from.test.ts new file mode 100644 index 00000000..90905a00 --- /dev/null +++ b/__test__/compose/service/sevice-volumes-from.test.ts @@ -0,0 +1,76 @@ +import { generateRandomHash } from "@/server/utils/docker/compose"; +import { addPrefixToServiceNames } from "@/server/utils/docker/compose/service"; +import type { ComposeSpecification } from "@/server/utils/docker/types"; +import { load } from "js-yaml"; +import { expect, test } from "vitest"; + +test("Generate random hash with 8 characters", () => { + const hash = generateRandomHash(); + + expect(hash).toBeDefined(); + expect(hash.length).toBe(8); +}); + +const composeFile3 = ` +version: "3.8" + +services: + web: + image: nginx:latest + volumes_from: + - shared + + api: + image: myapi:latest + volumes_from: + - shared + + shared: + image: busybox + volumes: + - /data + +networks: + default: + driver: bridge +`; + +test("Add prefix to service names with volumes_from in compose file", () => { + const composeData = load(composeFile3) as ComposeSpecification; + + const prefix = generateRandomHash(); + + if (!composeData.services) { + return; + } + const updatedComposeData = addPrefixToServiceNames( + composeData.services, + prefix, + ); + const actualComposeData = { ...composeData, services: updatedComposeData }; + + // Verificar que la nueva clave del servicio tiene el prefijo y la vieja clave no existe + expect(actualComposeData.services).toHaveProperty(`web-${prefix}`); + expect(actualComposeData.services).not.toHaveProperty("web"); + + // Verificar que la configuración de la imagen sigue igual + expect(actualComposeData.services[`web-${prefix}`].image).toBe( + "nginx:latest", + ); + expect(actualComposeData.services[`api-${prefix}`].image).toBe( + "myapi:latest", + ); + + // Verificar que los nombres en volumes_from tienen el prefijo + expect(actualComposeData.services[`web-${prefix}`].volumes_from).toContain( + `shared-${prefix}`, + ); + expect(actualComposeData.services[`api-${prefix}`].volumes_from).toContain( + `shared-${prefix}`, + ); + + // Verificar que el servicio shared también tiene el prefijo + expect(actualComposeData.services).toHaveProperty(`shared-${prefix}`); + expect(actualComposeData.services).not.toHaveProperty("shared"); + expect(actualComposeData.services[`shared-${prefix}`].image).toBe("busybox"); +}); diff --git a/__test__/compose/volume/volume-2.test.ts b/__test__/compose/volume/volume-2.test.ts new file mode 100644 index 00000000..41581844 --- /dev/null +++ b/__test__/compose/volume/volume-2.test.ts @@ -0,0 +1,1120 @@ +import { generateRandomHash } from "@/server/utils/docker/compose"; +import { + addPrefixToVolumesRoot, + addPrefixToAllVolumes, +} from "@/server/utils/docker/compose/volume"; +import type { ComposeSpecification } from "@/server/utils/docker/types"; +import { load } from "js-yaml"; +import { expect, test } from "vitest"; + +const composeFile = ` +services: + mail: + image: bytemark/smtp + restart: always + + plausible_db: + image: postgres:14-alpine + restart: always + volumes: + - db-data:/var/lib/postgresql/data + environment: + - POSTGRES_PASSWORD=postgres + + plausible_events_db: + image: clickhouse/clickhouse-server:23.3.7.5-alpine + restart: always + volumes: + - event-data:/var/lib/clickhouse + - event-logs:/var/log/clickhouse-server + - ./clickhouse/clickhouse-config.xml:/etc/clickhouse-server/config.d/logging.xml:ro + - ./clickhouse/clickhouse-user-config.xml:/etc/clickhouse-server/users.d/logging.xml:ro + ulimits: + nofile: + soft: 262144 + hard: 262144 + + plausible: + image: plausible/analytics:v2.0 + restart: always + command: sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh run" + depends_on: + - plausible_db + - plausible_events_db + - mail + ports: + - 127.0.0.1:8000:8000 + env_file: + - plausible-conf.env + volumes: + - type: volume + source: plausible-data + target: /data + + mysql: + image: mysql:5.7 + restart: always + environment: + MYSQL_ROOT_PASSWORD: example + volumes: + - type: volume + source: db-data + target: /var/lib/mysql/data + +volumes: + db-data: + driver: local + event-data: + driver: local + event-logs: + driver: local +`; + +const expectedDockerCompose = load(` +services: + mail: + image: bytemark/smtp + restart: always + + plausible_db: + image: postgres:14-alpine + restart: always + volumes: + - db-data-testhash:/var/lib/postgresql/data + environment: + - POSTGRES_PASSWORD=postgres + + plausible_events_db: + image: clickhouse/clickhouse-server:23.3.7.5-alpine + restart: always + volumes: + - event-data-testhash:/var/lib/clickhouse + - event-logs-testhash:/var/log/clickhouse-server + - ./clickhouse/clickhouse-config.xml:/etc/clickhouse-server/config.d/logging.xml:ro + - ./clickhouse/clickhouse-user-config.xml:/etc/clickhouse-server/users.d/logging.xml:ro + ulimits: + nofile: + soft: 262144 + hard: 262144 + + plausible: + image: plausible/analytics:v2.0 + restart: always + command: sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh run" + depends_on: + - plausible_db + - plausible_events_db + - mail + ports: + - 127.0.0.1:8000:8000 + env_file: + - plausible-conf.env + volumes: + - type: volume + source: plausible-data-testhash + target: /data + + mysql: + image: mysql:5.7 + restart: always + environment: + MYSQL_ROOT_PASSWORD: example + volumes: + - type: volume + source: db-data-testhash + target: /var/lib/mysql/data + +volumes: + db-data-testhash: + driver: local + event-data-testhash: + driver: local + event-logs-testhash: + driver: local +`) as ComposeSpecification; + +test("Generate random hash with 8 characters", () => { + const hash = generateRandomHash(); + + expect(hash).toBeDefined(); + expect(hash.length).toBe(8); +}); + +// Docker compose needs unique names for services, volumes, networks and containers +// So base on a input which is a dockercompose file, it should replace the name with a hash and return a new dockercompose file +test("Add prefix to volumes root property", () => { + const composeData = load(composeFile) as ComposeSpecification; + + const prefix = generateRandomHash(); + + if (!composeData?.volumes) { + return; + } + const volumes = addPrefixToVolumesRoot(composeData.volumes, prefix); + + // { + // 'db-data-af045046': { driver: 'local' }, + // 'event-data-af045046': { driver: 'local' }, + // 'event-logs-af045046': { driver: 'local' } + // } + + expect(volumes).toBeDefined(); + for (const volumeKey of Object.keys(volumes)) { + expect(volumeKey).toContain(`-${prefix}`); + } +}); + +test("Expect to change the prefix in all the possible places", () => { + const composeData = load(composeFile) as ComposeSpecification; + const prefix = "testhash"; + + const updatedComposeData = addPrefixToAllVolumes(composeData, prefix); + + expect(updatedComposeData).toEqual(expectedDockerCompose); +}); + +const composeFile2 = ` +version: '3.8' +services: + app: + image: myapp:latest + build: + context: . + dockerfile: Dockerfile + volumes: + - app-config:/usr/src/app/config + - ./config:/usr/src/app/config:ro + environment: + - NODE_ENV=production + mongo: + image: mongo:4.2 + volumes: + - mongo-data:/data/db +volumes: + app-config: + mongo-data: +`; + +const expectedDockerCompose2 = load(` +version: '3.8' +services: + app: + image: myapp:latest + build: + context: . + dockerfile: Dockerfile + volumes: + - app-config-testhash:/usr/src/app/config + - ./config:/usr/src/app/config:ro + environment: + - NODE_ENV=production + mongo: + image: mongo:4.2 + volumes: + - mongo-data-testhash:/data/db +volumes: + app-config-testhash: + mongo-data-testhash: +`) as ComposeSpecification; + +test("Expect to change the prefix in all the possible places (2 Try)", () => { + const composeData = load(composeFile2) as ComposeSpecification; + const prefix = "testhash"; + + const updatedComposeData = addPrefixToAllVolumes(composeData, prefix); + + expect(updatedComposeData).toEqual(expectedDockerCompose2); +}); + +const composeFile3 = ` +version: '3.8' +services: + app: + image: myapp:latest + build: + context: . + dockerfile: Dockerfile + volumes: + - app-config:/usr/src/app/config + - ./config:/usr/src/app/config:ro + environment: + - NODE_ENV=production + mongo: + image: mongo:4.2 + volumes: + - mongo-data:/data/db +volumes: + app-config: + mongo-data: +`; + +const expectedDockerCompose3 = load(` +version: '3.8' +services: + app: + image: myapp:latest + build: + context: . + dockerfile: Dockerfile + volumes: + - app-config-testhash:/usr/src/app/config + - ./config:/usr/src/app/config:ro + environment: + - NODE_ENV=production + mongo: + image: mongo:4.2 + volumes: + - mongo-data-testhash:/data/db +volumes: + app-config-testhash: + mongo-data-testhash: +`) as ComposeSpecification; + +test("Expect to change the prefix in all the possible places (3 Try)", () => { + const composeData = load(composeFile3) as ComposeSpecification; + const prefix = "testhash"; + + const updatedComposeData = addPrefixToAllVolumes(composeData, prefix); + + expect(updatedComposeData).toEqual(expectedDockerCompose3); +}); + +const composeFileComplex = ` +version: "3.8" +services: + studio: + container_name: supabase-studio + image: supabase/studio:20240422-5cf8f30 + restart: unless-stopped + healthcheck: + test: + [ + "CMD", + "node", + "-e", + "require('http').get('http://localhost:3000/api/profile', (r) => {if (r.statusCode !== 200) throw new Error(r.statusCode)})" + ] + timeout: 5s + interval: 5s + retries: 3 + depends_on: + analytics: + condition: service_healthy + environment: + STUDIO_PG_META_URL: http://meta:8080 + POSTGRES_PASSWORD: \${POSTGRES_PASSWORD} + DEFAULT_ORGANIZATION_NAME: \${STUDIO_DEFAULT_ORGANIZATION} + DEFAULT_PROJECT_NAME: \${STUDIO_DEFAULT_PROJECT} + SUPABASE_URL: http://kong:8000 + SUPABASE_PUBLIC_URL: \${SUPABASE_PUBLIC_URL} + SUPABASE_ANON_KEY: \${ANON_KEY} + SUPABASE_SERVICE_KEY: \${SERVICE_ROLE_KEY} + LOGFLARE_API_KEY: \${LOGFLARE_API_KEY} + LOGFLARE_URL: http://analytics:4000 + NEXT_PUBLIC_ENABLE_LOGS: true + NEXT_ANALYTICS_BACKEND_PROVIDER: postgres + + kong: + container_name: supabase-kong + image: kong:2.8.1 + restart: unless-stopped + entrypoint: bash -c 'eval "echo \"$$(cat ~/temp.yml)\"" > ~/kong.yml && /docker-entrypoint.sh kong docker-start' + ports: + - \${KONG_HTTP_PORT}:8000/tcp + - \${KONG_HTTPS_PORT}:8443/tcp + depends_on: + analytics: + condition: service_healthy + environment: + KONG_DATABASE: "off" + KONG_DECLARATIVE_CONFIG: /home/kong/kong.yml + KONG_DNS_ORDER: LAST,A,CNAME + KONG_PLUGINS: request-transformer,cors,key-auth,acl,basic-auth + KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: 160k + KONG_NGINX_PROXY_PROXY_BUFFERS: 64 160k + SUPABASE_ANON_KEY: \${ANON_KEY} + SUPABASE_SERVICE_KEY: \${SERVICE_ROLE_KEY} + DASHBOARD_USERNAME: \${DASHBOARD_USERNAME} + DASHBOARD_PASSWORD: \${DASHBOARD_PASSWORD} + volumes: + - ./volumes/api/kong.yml:/home/kong/temp.yml:ro + + auth: + container_name: supabase-auth + image: supabase/gotrue:v2.151.0 + depends_on: + db: + condition: service_healthy + analytics: + condition: service_healthy + healthcheck: + test: + [ + "CMD", + "wget", + "--no-verbose", + "--tries=1", + "--spider", + "http://localhost:9999/health" + ] + timeout: 5s + interval: 5s + retries: 3 + restart: unless-stopped + environment: + GOTRUE_API_HOST: 0.0.0.0 + GOTRUE_API_PORT: 9999 + API_EXTERNAL_URL: \${API_EXTERNAL_URL} + GOTRUE_DB_DRIVER: postgres + GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:\${POSTGRES_PASSWORD}@\${POSTGRES_HOST}:\${POSTGRES_PORT}/\${POSTGRES_DB} + GOTRUE_SITE_URL: \${SITE_URL} + GOTRUE_URI_ALLOW_LIST: \${ADDITIONAL_REDIRECT_URLS} + GOTRUE_DISABLE_SIGNUP: \${DISABLE_SIGNUP} + GOTRUE_JWT_ADMIN_ROLES: service_role + GOTRUE_JWT_AUD: authenticated + GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated + GOTRUE_JWT_EXP: \${JWT_EXPIRY} + GOTRUE_JWT_SECRET: \${JWT_SECRET} + GOTRUE_EXTERNAL_EMAIL_ENABLED: \${ENABLE_EMAIL_SIGNUP} + GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: \${ENABLE_ANONYMOUS_USERS} + GOTRUE_MAILER_AUTOCONFIRM: \${ENABLE_EMAIL_AUTOCONFIRM} + GOTRUE_SMTP_ADMIN_EMAIL: \${SMTP_ADMIN_EMAIL} + GOTRUE_SMTP_HOST: \${SMTP_HOST} + GOTRUE_SMTP_PORT: \${SMTP_PORT} + GOTRUE_SMTP_USER: \${SMTP_USER} + GOTRUE_SMTP_PASS: \${SMTP_PASS} + GOTRUE_SMTP_SENDER_NAME: \${SMTP_SENDER_NAME} + GOTRUE_MAILER_URLPATHS_INVITE: \${MAILER_URLPATHS_INVITE} + GOTRUE_MAILER_URLPATHS_CONFIRMATION: \${MAILER_URLPATHS_CONFIRMATION} + GOTRUE_MAILER_URLPATHS_RECOVERY: \${MAILER_URLPATHS_RECOVERY} + GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: \${MAILER_URLPATHS_EMAIL_CHANGE} + GOTRUE_EXTERNAL_PHONE_ENABLED: \${ENABLE_PHONE_SIGNUP} + GOTRUE_SMS_AUTOCONFIRM: \${ENABLE_PHONE_AUTOCONFIRM} + + rest: + container_name: supabase-rest + image: postgrest/postgrest:v12.0.1 + depends_on: + db: + condition: service_healthy + analytics: + condition: service_healthy + restart: unless-stopped + environment: + PGRST_DB_URI: postgres://authenticator:\${POSTGRES_PASSWORD}@\${POSTGRES_HOST}:\${POSTGRES_PORT}/\${POSTGRES_DB} + PGRST_DB_SCHEMAS: \${PGRST_DB_SCHEMAS} + PGRST_DB_ANON_ROLE: anon + PGRST_JWT_SECRET: \${JWT_SECRET} + PGRST_DB_USE_LEGACY_GUCS: "false" + PGRST_APP_SETTINGS_JWT_SECRET: \${JWT_SECRET} + PGRST_APP_SETTINGS_JWT_EXP: \${JWT_EXPIRY} + command: "postgrest" + + realtime: + container_name: realtime-dev.supabase-realtime + image: supabase/realtime:v2.28.32 + depends_on: + db: + condition: service_healthy + analytics: + condition: service_healthy + healthcheck: + test: + [ + "CMD", + "curl", + "-sSfL", + "--head", + "-o", + "/dev/null", + "-H", + "Authorization: Bearer \${ANON_KEY}", + "http://localhost:4000/api/tenants/realtime-dev/health" + ] + timeout: 5s + interval: 5s + retries: 3 + restart: unless-stopped + environment: + PORT: 4000 + DB_HOST: \${POSTGRES_HOST} + DB_PORT: \${POSTGRES_PORT} + DB_USER: supabase_admin + DB_PASSWORD: \${POSTGRES_PASSWORD} + DB_NAME: \${POSTGRES_DB} + DB_AFTER_CONNECT_QUERY: 'SET search_path TO _realtime' + DB_ENC_KEY: supabaserealtime + API_JWT_SECRET: \${JWT_SECRET} + FLY_ALLOC_ID: fly123 + FLY_APP_NAME: realtime + SECRET_KEY_BASE: UpNVntn3cDxHJpq99YMc1T1AQgQpc8kfYTuRgBiYa15BLrx8etQoXz3gZv1/u2oq + ERL_AFLAGS: -proto_dist inet_tcp + ENABLE_TAILSCALE: "false" + DNS_NODES: "''" + command: > + sh -c "/app/bin/migrate && /app/bin/realtime eval 'Realtime.Release.seeds(Realtime.Repo)' && /app/bin/server" + + storage: + container_name: supabase-storage + image: supabase/storage-api:v1.0.6 + depends_on: + db: + condition: service_healthy + rest: + condition: service_started + imgproxy: + condition: service_started + healthcheck: + test: + [ + "CMD", + "wget", + "--no-verbose", + "--tries=1", + "--spider", + "http://localhost:5000/status" + ] + timeout: 5s + interval: 5s + retries: 3 + restart: unless-stopped + environment: + ANON_KEY: \${ANON_KEY} + SERVICE_KEY: \${SERVICE_ROLE_KEY} + POSTGREST_URL: http://rest:3000 + PGRST_JWT_SECRET: \${JWT_SECRET} + DATABASE_URL: postgres://supabase_storage_admin:\${POSTGRES_PASSWORD}@\${POSTGRES_HOST}:\${POSTGRES_PORT}/\${POSTGRES_DB} + FILE_SIZE_LIMIT: 52428800 + STORAGE_BACKEND: file + FILE_STORAGE_BACKEND_PATH: /var/lib/storage + TENANT_ID: stub + REGION: stub + GLOBAL_S3_BUCKET: stub + ENABLE_IMAGE_TRANSFORMATION: "true" + IMGPROXY_URL: http://imgproxy:5001 + volumes: + - ./volumes/storage:/var/lib/storage:z + + imgproxy: + container_name: supabase-imgproxy + image: darthsim/imgproxy:v3.8.0 + healthcheck: + test: [ "CMD", "imgproxy", "health" ] + timeout: 5s + interval: 5s + retries: 3 + environment: + IMGPROXY_BIND: ":5001" + IMGPROXY_LOCAL_FILESYSTEM_ROOT: / + IMGPROXY_USE_ETAG: "true" + IMGPROXY_ENABLE_WEBP_DETECTION: \${IMGPROXY_ENABLE_WEBP_DETECTION} + volumes: + - ./volumes/storage:/var/lib/storage:z + + meta: + container_name: supabase-meta + image: supabase/postgres-meta:v0.80.0 + depends_on: + db: + condition: service_healthy + analytics: + condition: service_healthy + restart: unless-stopped + environment: + PG_META_PORT: 8080 + PG_META_DB_HOST: \${POSTGRES_HOST} + PG_META_DB_PORT: \${POSTGRES_PORT} + PG_META_DB_NAME: \${POSTGRES_DB} + PG_META_DB_USER: supabase_admin + PG_META_DB_PASSWORD: \${POSTGRES_PASSWORD} + + functions: + container_name: supabase-edge-functions + image: supabase/edge-runtime:v1.45.2 + restart: unless-stopped + depends_on: + analytics: + condition: service_healthy + environment: + JWT_SECRET: \${JWT_SECRET} + SUPABASE_URL: http://kong:8000 + SUPABASE_ANON_KEY: \${ANON_KEY} + SUPABASE_SERVICE_ROLE_KEY: \${SERVICE_ROLE_KEY} + SUPABASE_DB_URL: postgresql://postgres:\${POSTGRES_PASSWORD}@\${POSTGRES_HOST}:\${POSTGRES_PORT}/\${POSTGRES_DB} + VERIFY_JWT: "\${FUNCTIONS_VERIFY_JWT}" + volumes: + - ./volumes/functions:/home/deno/functions:Z + command: + - start + - --main-service + - /home/deno/functions/main + + analytics: + container_name: supabase-analytics + image: supabase/logflare:1.4.0 + healthcheck: + test: [ "CMD", "curl", "http://localhost:4000/health" ] + timeout: 5s + interval: 5s + retries: 10 + restart: unless-stopped + depends_on: + db: + condition: service_healthy + environment: + LOGFLARE_NODE_HOST: 127.0.0.1 + DB_USERNAME: supabase_admin + DB_DATABASE: \${POSTGRES_DB} + DB_HOSTNAME: \${POSTGRES_HOST} + DB_PORT: \${POSTGRES_PORT} + DB_PASSWORD: \${POSTGRES_PASSWORD} + DB_SCHEMA: _analytics + LOGFLARE_API_KEY: \${LOGFLARE_API_KEY} + LOGFLARE_SINGLE_TENANT: true + LOGFLARE_SUPABASE_MODE: true + LOGFLARE_MIN_CLUSTER_SIZE: 1 + POSTGRES_BACKEND_URL: postgresql://supabase_admin:\${POSTGRES_PASSWORD}@\${POSTGRES_HOST}:\${POSTGRES_PORT}/\${POSTGRES_DB} + POSTGRES_BACKEND_SCHEMA: _analytics + LOGFLARE_FEATURE_FLAG_OVERRIDE: multibackend=true + ports: + - 4000:4000 + + db: + container_name: supabase-db + image: supabase/postgres:15.1.1.41 + healthcheck: + test: pg_isready -U postgres -h localhost + interval: 5s + timeout: 5s + retries: 10 + depends_on: + vector: + condition: service_healthy + command: + - postgres + - -c + - config_file=/etc/postgresql/postgresql.conf + - -c + - log_min_messages=fatal + restart: unless-stopped + ports: + - \${POSTGRES_PORT}:\${POSTGRES_PORT} + environment: + POSTGRES_HOST: /var/run/postgresql + PGPORT: \${POSTGRES_PORT} + POSTGRES_PORT: \${POSTGRES_PORT} + PGPASSWORD: \${POSTGRES_PASSWORD} + POSTGRES_PASSWORD: \${POSTGRES_PASSWORD} + PGDATABASE: \${POSTGRES_DB} + POSTGRES_DB: \${POSTGRES_DB} + JWT_SECRET: \${JWT_SECRET} + JWT_EXP: \${JWT_EXPIRY} + volumes: + - ./volumes/db/realtime.sql:/docker-entrypoint-initdb.d/migrations/99-realtime.sql:Z + - ./volumes/db/webhooks.sql:/docker-entrypoint-initdb.d/init-scripts/98-webhooks.sql:Z + - ./volumes/db/roles.sql:/docker-entrypoint-initdb.d/init-scripts/99-roles.sql:Z + - ./volumes/db/jwt.sql:/docker-entrypoint-initdb.d/init-scripts/99-jwt.sql:Z + - ./volumes/db/data:/var/lib/postgresql/data:Z + - ./volumes/db/logs.sql:/docker-entrypoint-initdb.d/migrations/99-logs.sql:Z + - db-config:/etc/postgresql-custom + + vector: + container_name: supabase-vector + image: timberio/vector:0.28.1-alpine + healthcheck: + test: + [ + "CMD", + "wget", + "--no-verbose", + "--tries=1", + "--spider", + "http://vector:9001/health" + ] + timeout: 5s + interval: 5s + retries: 3 + volumes: + - ./volumes/logs/vector.yml:/etc/vector/vector.yml:ro + - "\${DOCKER_SOCKET_LOCATION}:/var/run/docker.sock:ro" + environment: + LOGFLARE_API_KEY: \${LOGFLARE_API_KEY} + command: [ "--config", "etc/vector/vector.yml" ] + +volumes: + db-config: +`; + +const expectedDockerComposeComplex = load(` +version: "3.8" +services: + studio: + container_name: supabase-studio + image: supabase/studio:20240422-5cf8f30 + restart: unless-stopped + healthcheck: + test: + [ + "CMD", + "node", + "-e", + "require('http').get('http://localhost:3000/api/profile', (r) => {if (r.statusCode !== 200) throw new Error(r.statusCode)})" + ] + timeout: 5s + interval: 5s + retries: 3 + depends_on: + analytics: + condition: service_healthy + environment: + STUDIO_PG_META_URL: http://meta:8080 + POSTGRES_PASSWORD: \${POSTGRES_PASSWORD} + DEFAULT_ORGANIZATION_NAME: \${STUDIO_DEFAULT_ORGANIZATION} + DEFAULT_PROJECT_NAME: \${STUDIO_DEFAULT_PROJECT} + SUPABASE_URL: http://kong:8000 + SUPABASE_PUBLIC_URL: \${SUPABASE_PUBLIC_URL} + SUPABASE_ANON_KEY: \${ANON_KEY} + SUPABASE_SERVICE_KEY: \${SERVICE_ROLE_KEY} + LOGFLARE_API_KEY: \${LOGFLARE_API_KEY} + LOGFLARE_URL: http://analytics:4000 + NEXT_PUBLIC_ENABLE_LOGS: true + NEXT_ANALYTICS_BACKEND_PROVIDER: postgres + + kong: + container_name: supabase-kong + image: kong:2.8.1 + restart: unless-stopped + entrypoint: bash -c 'eval "echo \"$$(cat ~/temp.yml)\"" > ~/kong.yml && /docker-entrypoint.sh kong docker-start' + ports: + - \${KONG_HTTP_PORT}:8000/tcp + - \${KONG_HTTPS_PORT}:8443/tcp + depends_on: + analytics: + condition: service_healthy + environment: + KONG_DATABASE: "off" + KONG_DECLARATIVE_CONFIG: /home/kong/kong.yml + KONG_DNS_ORDER: LAST,A,CNAME + KONG_PLUGINS: request-transformer,cors,key-auth,acl,basic-auth + KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: 160k + KONG_NGINX_PROXY_PROXY_BUFFERS: 64 160k + SUPABASE_ANON_KEY: \${ANON_KEY} + SUPABASE_SERVICE_KEY: \${SERVICE_ROLE_KEY} + DASHBOARD_USERNAME: \${DASHBOARD_USERNAME} + DASHBOARD_PASSWORD: \${DASHBOARD_PASSWORD} + volumes: + - ./volumes/api/kong.yml:/home/kong/temp.yml:ro + + auth: + container_name: supabase-auth + image: supabase/gotrue:v2.151.0 + depends_on: + db: + condition: service_healthy + analytics: + condition: service_healthy + healthcheck: + test: + [ + "CMD", + "wget", + "--no-verbose", + "--tries=1", + "--spider", + "http://localhost:9999/health" + ] + timeout: 5s + interval: 5s + retries: 3 + restart: unless-stopped + environment: + GOTRUE_API_HOST: 0.0.0.0 + GOTRUE_API_PORT: 9999 + API_EXTERNAL_URL: \${API_EXTERNAL_URL} + GOTRUE_DB_DRIVER: postgres + GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:\${POSTGRES_PASSWORD}@\${POSTGRES_HOST}:\${POSTGRES_PORT}/\${POSTGRES_DB} + GOTRUE_SITE_URL: \${SITE_URL} + GOTRUE_URI_ALLOW_LIST: \${ADDITIONAL_REDIRECT_URLS} + GOTRUE_DISABLE_SIGNUP: \${DISABLE_SIGNUP} + GOTRUE_JWT_ADMIN_ROLES: service_role + GOTRUE_JWT_AUD: authenticated + GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated + GOTRUE_JWT_EXP: \${JWT_EXPIRY} + GOTRUE_JWT_SECRET: \${JWT_SECRET} + GOTRUE_EXTERNAL_EMAIL_ENABLED: \${ENABLE_EMAIL_SIGNUP} + GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: \${ENABLE_ANONYMOUS_USERS} + GOTRUE_MAILER_AUTOCONFIRM: \${ENABLE_EMAIL_AUTOCONFIRM} + GOTRUE_SMTP_ADMIN_EMAIL: \${SMTP_ADMIN_EMAIL} + GOTRUE_SMTP_HOST: \${SMTP_HOST} + GOTRUE_SMTP_PORT: \${SMTP_PORT} + GOTRUE_SMTP_USER: \${SMTP_USER} + GOTRUE_SMTP_PASS: \${SMTP_PASS} + GOTRUE_SMTP_SENDER_NAME: \${SMTP_SENDER_NAME} + GOTRUE_MAILER_URLPATHS_INVITE: \${MAILER_URLPATHS_INVITE} + GOTRUE_MAILER_URLPATHS_CONFIRMATION: \${MAILER_URLPATHS_CONFIRMATION} + GOTRUE_MAILER_URLPATHS_RECOVERY: \${MAILER_URLPATHS_RECOVERY} + GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: \${MAILER_URLPATHS_EMAIL_CHANGE} + GOTRUE_EXTERNAL_PHONE_ENABLED: \${ENABLE_PHONE_SIGNUP} + GOTRUE_SMS_AUTOCONFIRM: \${ENABLE_PHONE_AUTOCONFIRM} + + rest: + container_name: supabase-rest + image: postgrest/postgrest:v12.0.1 + depends_on: + db: + condition: service_healthy + analytics: + condition: service_healthy + restart: unless-stopped + environment: + PGRST_DB_URI: postgres://authenticator:\${POSTGRES_PASSWORD}@\${POSTGRES_HOST}:\${POSTGRES_PORT}/\${POSTGRES_DB} + PGRST_DB_SCHEMAS: \${PGRST_DB_SCHEMAS} + PGRST_DB_ANON_ROLE: anon + PGRST_JWT_SECRET: \${JWT_SECRET} + PGRST_DB_USE_LEGACY_GUCS: "false" + PGRST_APP_SETTINGS_JWT_SECRET: \${JWT_SECRET} + PGRST_APP_SETTINGS_JWT_EXP: \${JWT_EXPIRY} + command: "postgrest" + + realtime: + container_name: realtime-dev.supabase-realtime + image: supabase/realtime:v2.28.32 + depends_on: + db: + condition: service_healthy + analytics: + condition: service_healthy + healthcheck: + test: + [ + "CMD", + "curl", + "-sSfL", + "--head", + "-o", + "/dev/null", + "-H", + "Authorization: Bearer \${ANON_KEY}", + "http://localhost:4000/api/tenants/realtime-dev/health" + ] + timeout: 5s + interval: 5s + retries: 3 + restart: unless-stopped + environment: + PORT: 4000 + DB_HOST: \${POSTGRES_HOST} + DB_PORT: \${POSTGRES_PORT} + DB_USER: supabase_admin + DB_PASSWORD: \${POSTGRES_PASSWORD} + DB_NAME: \${POSTGRES_DB} + DB_AFTER_CONNECT_QUERY: 'SET search_path TO _realtime' + DB_ENC_KEY: supabaserealtime + API_JWT_SECRET: \${JWT_SECRET} + FLY_ALLOC_ID: fly123 + FLY_APP_NAME: realtime + SECRET_KEY_BASE: UpNVntn3cDxHJpq99YMc1T1AQgQpc8kfYTuRgBiYa15BLrx8etQoXz3gZv1/u2oq + ERL_AFLAGS: -proto_dist inet_tcp + ENABLE_TAILSCALE: "false" + DNS_NODES: "''" + command: > + sh -c "/app/bin/migrate && /app/bin/realtime eval 'Realtime.Release.seeds(Realtime.Repo)' && /app/bin/server" + + storage: + container_name: supabase-storage + image: supabase/storage-api:v1.0.6 + depends_on: + db: + condition: service_healthy + rest: + condition: service_started + imgproxy: + condition: service_started + healthcheck: + test: + [ + "CMD", + "wget", + "--no-verbose", + "--tries=1", + "--spider", + "http://localhost:5000/status" + ] + timeout: 5s + interval: 5s + retries: 3 + restart: unless-stopped + environment: + ANON_KEY: \${ANON_KEY} + SERVICE_KEY: \${SERVICE_ROLE_KEY} + POSTGREST_URL: http://rest:3000 + PGRST_JWT_SECRET: \${JWT_SECRET} + DATABASE_URL: postgres://supabase_storage_admin:\${POSTGRES_PASSWORD}@\${POSTGRES_HOST}:\${POSTGRES_PORT}/\${POSTGRES_DB} + FILE_SIZE_LIMIT: 52428800 + STORAGE_BACKEND: file + FILE_STORAGE_BACKEND_PATH: /var/lib/storage + TENANT_ID: stub + REGION: stub + GLOBAL_S3_BUCKET: stub + ENABLE_IMAGE_TRANSFORMATION: "true" + IMGPROXY_URL: http://imgproxy:5001 + volumes: + - ./volumes/storage:/var/lib/storage:z + + imgproxy: + container_name: supabase-imgproxy + image: darthsim/imgproxy:v3.8.0 + healthcheck: + test: [ "CMD", "imgproxy", "health" ] + timeout: 5s + interval: 5s + retries: 3 + environment: + IMGPROXY_BIND: ":5001" + IMGPROXY_LOCAL_FILESYSTEM_ROOT: / + IMGPROXY_USE_ETAG: "true" + IMGPROXY_ENABLE_WEBP_DETECTION: \${IMGPROXY_ENABLE_WEBP_DETECTION} + volumes: + - ./volumes/storage:/var/lib/storage:z + + meta: + container_name: supabase-meta + image: supabase/postgres-meta:v0.80.0 + depends_on: + db: + condition: service_healthy + analytics: + condition: service_healthy + restart: unless-stopped + environment: + PG_META_PORT: 8080 + PG_META_DB_HOST: \${POSTGRES_HOST} + PG_META_DB_PORT: \${POSTGRES_PORT} + PG_META_DB_NAME: \${POSTGRES_DB} + PG_META_DB_USER: supabase_admin + PG_META_DB_PASSWORD: \${POSTGRES_PASSWORD} + + functions: + container_name: supabase-edge-functions + image: supabase/edge-runtime:v1.45.2 + restart: unless-stopped + depends_on: + analytics: + condition: service_healthy + environment: + JWT_SECRET: \${JWT_SECRET} + SUPABASE_URL: http://kong:8000 + SUPABASE_ANON_KEY: \${ANON_KEY} + SUPABASE_SERVICE_ROLE_KEY: \${SERVICE_ROLE_KEY} + SUPABASE_DB_URL: postgresql://postgres:\${POSTGRES_PASSWORD}@\${POSTGRES_HOST}:\${POSTGRES_PORT}/\${POSTGRES_DB} + VERIFY_JWT: "\${FUNCTIONS_VERIFY_JWT}" + volumes: + - ./volumes/functions:/home/deno/functions:Z + command: + - start + - --main-service + - /home/deno/functions/main + + analytics: + container_name: supabase-analytics + image: supabase/logflare:1.4.0 + healthcheck: + test: [ "CMD", "curl", "http://localhost:4000/health" ] + timeout: 5s + interval: 5s + retries: 10 + restart: unless-stopped + depends_on: + db: + condition: service_healthy + environment: + LOGFLARE_NODE_HOST: 127.0.0.1 + DB_USERNAME: supabase_admin + DB_DATABASE: \${POSTGRES_DB} + DB_HOSTNAME: \${POSTGRES_HOST} + DB_PORT: \${POSTGRES_PORT} + DB_PASSWORD: \${POSTGRES_PASSWORD} + DB_SCHEMA: _analytics + LOGFLARE_API_KEY: \${LOGFLARE_API_KEY} + LOGFLARE_SINGLE_TENANT: true + LOGFLARE_SUPABASE_MODE: true + LOGFLARE_MIN_CLUSTER_SIZE: 1 + POSTGRES_BACKEND_URL: postgresql://supabase_admin:\${POSTGRES_PASSWORD}@\${POSTGRES_HOST}:\${POSTGRES_PORT}/\${POSTGRES_DB} + POSTGRES_BACKEND_SCHEMA: _analytics + LOGFLARE_FEATURE_FLAG_OVERRIDE: multibackend=true + ports: + - 4000:4000 + + db: + container_name: supabase-db + image: supabase/postgres:15.1.1.41 + healthcheck: + test: pg_isready -U postgres -h localhost + interval: 5s + timeout: 5s + retries: 10 + depends_on: + vector: + condition: service_healthy + command: + - postgres + - -c + - config_file=/etc/postgresql/postgresql.conf + - -c + - log_min_messages=fatal + restart: unless-stopped + ports: + - \${POSTGRES_PORT}:\${POSTGRES_PORT} + environment: + POSTGRES_HOST: /var/run/postgresql + PGPORT: \${POSTGRES_PORT} + POSTGRES_PORT: \${POSTGRES_PORT} + PGPASSWORD: \${POSTGRES_PASSWORD} + POSTGRES_PASSWORD: \${POSTGRES_PASSWORD} + PGDATABASE: \${POSTGRES_DB} + POSTGRES_DB: \${POSTGRES_DB} + JWT_SECRET: \${JWT_SECRET} + JWT_EXP: \${JWT_EXPIRY} + volumes: + - ./volumes/db/realtime.sql:/docker-entrypoint-initdb.d/migrations/99-realtime.sql:Z + - ./volumes/db/webhooks.sql:/docker-entrypoint-initdb.d/init-scripts/98-webhooks.sql:Z + - ./volumes/db/roles.sql:/docker-entrypoint-initdb.d/init-scripts/99-roles.sql:Z + - ./volumes/db/jwt.sql:/docker-entrypoint-initdb.d/init-scripts/99-jwt.sql:Z + - ./volumes/db/data:/var/lib/postgresql/data:Z + - ./volumes/db/logs.sql:/docker-entrypoint-initdb.d/migrations/99-logs.sql:Z + - db-config-testhash:/etc/postgresql-custom + + vector: + container_name: supabase-vector + image: timberio/vector:0.28.1-alpine + healthcheck: + test: + [ + "CMD", + "wget", + "--no-verbose", + "--tries=1", + "--spider", + "http://vector:9001/health" + ] + timeout: 5s + interval: 5s + retries: 3 + volumes: + - ./volumes/logs/vector.yml:/etc/vector/vector.yml:ro + - \${DOCKER_SOCKET_LOCATION}:/var/run/docker.sock:ro + environment: + LOGFLARE_API_KEY: \${LOGFLARE_API_KEY} + command: [ "--config", "etc/vector/vector.yml" ] + +volumes: + db-config-testhash: +`) as ComposeSpecification; + +test("Expect to change the prefix in all the possible places (4 Try)", () => { + const composeData = load(composeFileComplex) as ComposeSpecification; + const prefix = "testhash"; + + const updatedComposeData = addPrefixToAllVolumes(composeData, prefix); + + expect(updatedComposeData).toEqual(expectedDockerComposeComplex); +}); + +const composeFileExample1 = ` +version: "3.8" +services: + web: + image: nginx:latest + ports: + - "80:80" + networks: + - frontend + volumes: + - web-data:/var/www/html + - ./nginx.conf:/etc/nginx/nginx.conf:ro + + app: + image: node:14 + depends_on: + - db + networks: + - backend + - frontend + volumes: + - app-data:/usr/src/app + - ./src:/usr/src/app/src + + db: + image: postgres:13 + environment: + POSTGRES_PASSWORD: example + networks: + - backend + volumes: + - db-data:/var/lib/postgresql/data + +networks: + frontend: + driver: bridge + backend: + driver: bridge + +volumes: + web-data: + app-data: + db-data: +`; + +const expectedDockerComposeExample1 = load(` +version: "3.8" +services: + web: + image: nginx:latest + ports: + - "80:80" + networks: + - frontend + volumes: + - web-data-testhash:/var/www/html + - ./nginx.conf:/etc/nginx/nginx.conf:ro + + app: + image: node:14 + depends_on: + - db + networks: + - backend + - frontend + volumes: + - app-data-testhash:/usr/src/app + - ./src:/usr/src/app/src + + db: + image: postgres:13 + environment: + POSTGRES_PASSWORD: example + networks: + - backend + volumes: + - db-data-testhash:/var/lib/postgresql/data + +networks: + frontend: + driver: bridge + backend: + driver: bridge + +volumes: + web-data-testhash: + app-data-testhash: + db-data-testhash: +`) as ComposeSpecification; + +test("Expect to change the prefix in all the possible places (5 Try)", () => { + const composeData = load(composeFileExample1) as ComposeSpecification; + const prefix = "testhash"; + + const updatedComposeData = addPrefixToAllVolumes(composeData, prefix); + + expect(updatedComposeData).toEqual(expectedDockerComposeExample1); +}); diff --git a/__test__/compose/volume/volume-root.test.ts b/__test__/compose/volume/volume-root.test.ts new file mode 100644 index 00000000..6b87f2ca --- /dev/null +++ b/__test__/compose/volume/volume-root.test.ts @@ -0,0 +1,195 @@ +import { generateRandomHash } from "@/server/utils/docker/compose"; +import { addPrefixToVolumesRoot } from "@/server/utils/docker/compose/volume"; +import type { ComposeSpecification } from "@/server/utils/docker/types"; +import { load } from "js-yaml"; +import { expect, test } from "vitest"; + +const composeFile = ` +version: "3.8" + +services: + web: + image: nginx:latest + volumes: + - web_data:/var/lib/nginx/data + +volumes: + web_data: + driver: local + +networks: + default: + driver: bridge +`; + +test("Generate random hash with 8 characters", () => { + const hash = generateRandomHash(); + + expect(hash).toBeDefined(); + expect(hash.length).toBe(8); +}); + +test("Add prefix to volumes in root property", () => { + const composeData = load(composeFile) as ComposeSpecification; + + const prefix = generateRandomHash(); + + if (!composeData?.volumes) { + return; + } + const volumes = addPrefixToVolumesRoot(composeData.volumes, prefix); + expect(volumes).toBeDefined(); + for (const volumeKey of Object.keys(volumes)) { + expect(volumeKey).toContain(`-${prefix}`); + expect(volumes[volumeKey]).toBeDefined(); + } +}); + +const composeFile2 = ` +version: "3.8" + +services: + app: + image: node:latest + volumes: + - app_data:/var/lib/app/data + +volumes: + app_data: + driver: local + driver_opts: + type: nfs + o: addr=10.0.0.1,rw + device: ":/exported/path" + +networks: + default: + driver: bridge +`; + +test("Add prefix to volumes in root property (Case 2)", () => { + const composeData = load(composeFile2) as ComposeSpecification; + + const prefix = generateRandomHash(); + + if (!composeData?.volumes) { + return; + } + const volumes = addPrefixToVolumesRoot(composeData.volumes, prefix); + expect(volumes).toBeDefined(); + for (const volumeKey of Object.keys(volumes)) { + expect(volumeKey).toContain(`-${prefix}`); + expect(volumes[volumeKey]).toBeDefined(); + } +}); + +const composeFile3 = ` +version: "3.8" + +services: + db: + image: postgres:latest + volumes: + - db_data:/var/lib/postgresql/data + +volumes: + db_data: + external: true + +networks: + default: + driver: bridge +`; + +test("Add prefix to volumes in root property (Case 3)", () => { + const composeData = load(composeFile3) as ComposeSpecification; + + const prefix = generateRandomHash(); + + if (!composeData?.volumes) { + return; + } + const volumes = addPrefixToVolumesRoot(composeData.volumes, prefix); + + expect(volumes).toBeDefined(); + for (const volumeKey of Object.keys(volumes)) { + expect(volumeKey).toContain(`-${prefix}`); + expect(volumes[volumeKey]).toBeDefined(); + } +}); + +const composeFile4 = ` +version: "3.8" + +services: + web: + image: nginx:latest + + app: + image: node:latest + + db: + image: postgres:latest + +volumes: + web_data: + driver: local + + app_data: + driver: local + driver_opts: + type: nfs + o: addr=10.0.0.1,rw + device: ":/exported/path" + + db_data: + external: true + + +`; + +// Expected compose file con el prefijo `testhash` +const expectedComposeFile4 = load(` +version: "3.8" + +services: + web: + image: nginx:latest + + app: + image: node:latest + + db: + image: postgres:latest + +volumes: + web_data-testhash: + driver: local + + app_data-testhash: + driver: local + driver_opts: + type: nfs + o: addr=10.0.0.1,rw + device: ":/exported/path" + + db_data-testhash: + external: true + + +`) as ComposeSpecification; + +test("Add prefix to volumes in root property", () => { + const composeData = load(composeFile4) as ComposeSpecification; + + const prefix = "testhash"; + + if (!composeData?.volumes) { + return; + } + const volumes = addPrefixToVolumesRoot(composeData.volumes, prefix); + const updatedComposeData = { ...composeData, volumes }; + + // Verificar que el resultado coincide con el archivo esperado + expect(updatedComposeData).toEqual(expectedComposeFile4); +}); diff --git a/__test__/compose/volume/volume-services.test.ts b/__test__/compose/volume/volume-services.test.ts new file mode 100644 index 00000000..331c3bf1 --- /dev/null +++ b/__test__/compose/volume/volume-services.test.ts @@ -0,0 +1,81 @@ +import { generateRandomHash } from "@/server/utils/docker/compose"; +import { addPrefixToVolumesInServices } from "@/server/utils/docker/compose/volume"; +import type { ComposeSpecification } from "@/server/utils/docker/types"; +import { load } from "js-yaml"; +import { expect, test } from "vitest"; + +test("Generate random hash with 8 characters", () => { + const hash = generateRandomHash(); + + expect(hash).toBeDefined(); + expect(hash.length).toBe(8); +}); + +const composeFile1 = ` +version: "3.8" + +services: + db: + image: postgres:latest + volumes: + - db_data:/var/lib/postgresql/data +`; + +test("Add prefix to volumes declared directly in services", () => { + const composeData = load(composeFile1) as ComposeSpecification; + + const prefix = generateRandomHash(); + + if (!composeData.services) { + return; + } + + const updatedComposeData = addPrefixToVolumesInServices( + composeData.services, + prefix, + ); + const actualComposeData = { ...composeData, services: updatedComposeData }; + expect(actualComposeData.services?.db?.volumes).toContain( + `db_data-${prefix}:/var/lib/postgresql/data`, + ); +}); + +const composeFileTypeVolume = ` +version: "3.8" + +services: + db: + image: postgres:latest + volumes: + - type: volume + source: db-test + target: /var/lib/postgresql/data + +volumes: + db-test: + driver: local +`; + +test("Add prefix to volumes declared directly in services (Case 2)", () => { + const composeData = load(composeFileTypeVolume) as ComposeSpecification; + + const prefix = generateRandomHash(); + + if (!composeData.services) { + return; + } + + const updatedComposeData = addPrefixToVolumesInServices( + composeData.services, + prefix, + ); + const actualComposeData = { ...composeData, services: updatedComposeData }; + + expect(actualComposeData.services?.db?.volumes).toEqual([ + { + type: "volume", + source: `db-test-${prefix}`, + target: "/var/lib/postgresql/data", + }, + ]); +}); diff --git a/__test__/compose/volume/volume.test.ts b/__test__/compose/volume/volume.test.ts new file mode 100644 index 00000000..98a23588 --- /dev/null +++ b/__test__/compose/volume/volume.test.ts @@ -0,0 +1,288 @@ +import { generateRandomHash } from "@/server/utils/docker/compose"; +import { + addPrefixToAllVolumes, + addPrefixToVolumesInServices, +} from "@/server/utils/docker/compose/volume"; +import type { ComposeSpecification } from "@/server/utils/docker/types"; +import { load } from "js-yaml"; +import { expect, test } from "vitest"; + +const composeFileTypeVolume = ` +version: "3.8" + +services: + db1: + image: postgres:latest + volumes: + - "db-test:/var/lib/postgresql/data" + db2: + image: postgres:latest + volumes: + - type: volume + source: db-test + target: /var/lib/postgresql/data + +volumes: + db-test: + driver: local +`; + +const expectedComposeFileTypeVolume = load(` +version: "3.8" + +services: + db1: + image: postgres:latest + volumes: + - "db-test-testhash:/var/lib/postgresql/data" + db2: + image: postgres:latest + volumes: + - type: volume + source: db-test-testhash + target: /var/lib/postgresql/data + +volumes: + db-test-testhash: + driver: local +`) as ComposeSpecification; + +test("Add prefix to volumes with type: volume in services", () => { + const composeData = load(composeFileTypeVolume) as ComposeSpecification; + + const prefix = "testhash"; + + const updatedComposeData = addPrefixToAllVolumes(composeData, prefix); + const actualComposeData = { ...composeData, ...updatedComposeData }; + + expect(actualComposeData).toEqual(expectedComposeFileTypeVolume); +}); + +const composeFileTypeVolume1 = ` +version: "3.8" + +services: + web: + image: nginx:latest + volumes: + - "web-data:/var/www/html" + - type: volume + source: web-logs + target: /var/log/nginx + +volumes: + web-data: + driver: local + web-logs: + driver: local +`; + +const expectedComposeFileTypeVolume1 = load(` +version: "3.8" + +services: + web: + image: nginx:latest + volumes: + - "web-data-testhash:/var/www/html" + - type: volume + source: web-logs-testhash + target: /var/log/nginx + +volumes: + web-data-testhash: + driver: local + web-logs-testhash: + driver: local +`) as ComposeSpecification; + +test("Add prefix to mixed volumes in services", () => { + const composeData = load(composeFileTypeVolume1) as ComposeSpecification; + + const prefix = "testhash"; + + const updatedComposeData = addPrefixToAllVolumes(composeData, prefix); + const actualComposeData = { ...composeData, ...updatedComposeData }; + + expect(actualComposeData).toEqual(expectedComposeFileTypeVolume1); +}); + +const composeFileTypeVolume2 = ` +version: "3.8" + +services: + app: + image: node:latest + volumes: + - "app-data:/usr/src/app" + - type: volume + source: app-logs + target: /var/log/app + volume: + nocopy: true + +volumes: + app-data: + driver: local + app-logs: + driver: local + driver_opts: + o: bind + type: none + device: /path/to/app/logs +`; + +const expectedComposeFileTypeVolume2 = load(` +version: "3.8" + +services: + app: + image: node:latest + volumes: + - "app-data-testhash:/usr/src/app" + - type: volume + source: app-logs-testhash + target: /var/log/app + volume: + nocopy: true + +volumes: + app-data-testhash: + driver: local + app-logs-testhash: + driver: local + driver_opts: + o: bind + type: none + device: /path/to/app/logs +`) as ComposeSpecification; + +test("Add prefix to complex volume configurations in services", () => { + const composeData = load(composeFileTypeVolume2) as ComposeSpecification; + + const prefix = "testhash"; + + const updatedComposeData = addPrefixToAllVolumes(composeData, prefix); + const actualComposeData = { ...composeData, ...updatedComposeData }; + + expect(actualComposeData).toEqual(expectedComposeFileTypeVolume2); +}); + +const composeFileTypeVolume3 = ` +version: "3.8" + +services: + web: + image: nginx:latest + volumes: + - "web-data:/usr/share/nginx/html" + - type: volume + source: web-logs + target: /var/log/nginx + volume: + nocopy: true + + api: + image: node:latest + volumes: + - "api-data:/usr/src/app" + - type: volume + source: api-logs + target: /var/log/app + volume: + nocopy: true + - type: volume + source: shared-logs + target: /shared/logs + +volumes: + web-data: + driver: local + web-logs: + driver: local + driver_opts: + o: bind + type: none + device: /path/to/web/logs + + api-data: + driver: local + api-logs: + driver: local + driver_opts: + o: bind + type: none + device: /path/to/api/logs + + shared-logs: + driver: local + driver_opts: + o: bind + type: none + device: /path/to/shared/logs +`; + +const expectedComposeFileTypeVolume3 = load(` +version: "3.8" + +services: + web: + image: nginx:latest + volumes: + - "web-data-testhash:/usr/share/nginx/html" + - type: volume + source: web-logs-testhash + target: /var/log/nginx + volume: + nocopy: true + + api: + image: node:latest + volumes: + - "api-data-testhash:/usr/src/app" + - type: volume + source: api-logs-testhash + target: /var/log/app + volume: + nocopy: true + - type: volume + source: shared-logs-testhash + target: /shared/logs + +volumes: + web-data-testhash: + driver: local + web-logs-testhash: + driver: local + driver_opts: + o: bind + type: none + device: /path/to/web/logs + + api-data-testhash: + driver: local + api-logs-testhash: + driver: local + driver_opts: + o: bind + type: none + device: /path/to/api/logs + + shared-logs-testhash: + driver: local + driver_opts: + o: bind + type: none + device: /path/to/shared/logs +`) as ComposeSpecification; + +test("Add prefix to complex nested volumes configuration in services", () => { + const composeData = load(composeFileTypeVolume3) as ComposeSpecification; + + const prefix = "testhash"; + + const updatedComposeData = addPrefixToAllVolumes(composeData, prefix); + const actualComposeData = { ...composeData, ...updatedComposeData }; + + expect(actualComposeData).toEqual(expectedComposeFileTypeVolume3); +}); diff --git a/__test__/vitest.config.ts b/__test__/vitest.config.ts new file mode 100644 index 00000000..4127903f --- /dev/null +++ b/__test__/vitest.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from "vitest/config"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig({ + plugins: [ + tsconfigPaths({ + root: "./", + projects: ["tsconfig.json"], + }), + ], + test: { + include: ["__test__/**/*.test.ts"], // Incluir solo los archivos de test en el directorio __test__ + exclude: ["**/node_modules/**", "**/dist/**", "**/.docker/**"], + pool: "forks", + }, +}); diff --git a/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx b/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx index e556650b..3e2730c9 100644 --- a/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx +++ b/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx @@ -24,7 +24,6 @@ import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; -import { Textarea } from "@/components/ui/textarea"; import { HelpCircle, Settings } from "lucide-react"; import { Tooltip, @@ -32,6 +31,7 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; +import { CodeEditor } from "@/components/shared/code-editor"; const HealthCheckSwarmSchema = z .object({ @@ -320,8 +320,8 @@ export const AddSwarmSettings = ({ applicationId }: Props) => { -