diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..97379020e --- /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 02d9bce92..364918135 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/.gitignore b/.gitignore index bfbda236c..4e788d919 100644 --- a/.gitignore +++ b/.gitignore @@ -53,4 +53,6 @@ yarn-error.log* /.data /.main -*.lockb \ No newline at end of file +*.lockb +*.rdb +.idea diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bbda3053c..8686b98a8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -107,7 +107,7 @@ pnpm run docker:push In the case you lost your password, you can reset it using the following command ```bash -pnpm run build-server +pnpm run reset-password ``` If you want to test the webhooks on development mode using localtunnel, make sure to install `localtunnel` @@ -150,4 +150,93 @@ curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.32.1/pack-v0. - If your pull request fixes an open issue, please reference the issue in the pull request description. - Once your pull request is merged, you will be automatically added as a contributor to the project. -Thank you for your contribution! \ No newline at end of file +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/Dockerfile b/Dockerfile index 97d7b26a7..6d3e58758 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,7 +26,7 @@ FROM node:18-slim AS production # Install dependencies only for production ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" -RUN corepack enable && apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* +RUN corepack enable && apt-get update && apt-get install -y curl && apt-get install -y apache2-utils && rm -rf /var/lib/apt/lists/* WORKDIR /app @@ -47,7 +47,6 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-l # Install docker RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm get-docker.sh - # Install Nixpacks and tsx # | VERBOSE=1 VERSION=1.21.0 bash RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \ diff --git a/LICENSE.MD b/LICENSE.MD index abf659ebf..9031c94b9 100644 --- a/LICENSE.MD +++ b/LICENSE.MD @@ -1,4 +1,8 @@ -Copyright 2024-2024 Mauricio Siu. +# License + +## Core License (Apache License 2.0) + +Copyright 2024 Mauricio Siu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -9,27 +13,14 @@ You may obtain a copy of the License at Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +See the License for the specific language governing permissions and limitations under the License. -Appendix: -In case of a conflict, the terms of this appendix supersede the general apache license. +## Additional Terms for Specific Features -- Unless provided with a written agreement or permission, the paid features of Dokploy (as a service) cannot be modified. -- Unless provided with a written agreement or permission, no persons are permitted to redistribute another paid version of Dokploy. -- Unless provided with a written agreement or permission, no persons are permitted to create the same paid version of Dokploy. -- Furthermore, any modifications of free features of Dokploy should be distributed as free & opensource software. - -Appendix B: - -- **Prohibition of Resale Without Permission:** Notwithstanding any provisions in the main body of the Apache License, Version 2.0, no party is permitted to sell, resell, or otherwise distribute for commercial gain, the software or any of its components, including both original and modified versions, through any form of commercial distribution channels, including but not limited to online marketplaces, software as a service (SaaS) platforms, or physical media distribution, without prior written consent from the copyright holder. - -- **Commercial Distribution:** Any form of distribution of Dokploy, whether for direct profit or indirect financial benefit, through commercial channels is strictly prohibited without a separate commercial agreement negotiated with the copyright holder. This includes but is not limited to, offerings on software marketplaces or through third-party distributors. - -- **Modification of Paid Features:** The paid features of Dokploy (as a service) may not be modified, integrated into other software, or redistributed in any form without explicit written permission from the copyright holder. - -- **Open Source Distribution of Free Features:** Any modifications to the free features of Dokploy must be distributed freely and must not be included in any paid or commercial package without complying with the open-source license terms stipulated in this agreement. - -If you have any questions, please feel free to reach out to us. +The following additional terms apply to the multi-node support and Docker Compose file support features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License: +- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support and Docker Compose file support, will always be free to use in the self-hosted version. +- **Restriction on Resale**: The multi-node support and Docker Compose file support features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent. +- **Modification Distribution**: Any modifications to the multi-node support and Docker Compose file support features must be distributed freely and cannot be sold or offered as a service. +For further inquiries or permissions, please contact us directly. diff --git a/README-de.md b/README-de.md new file mode 100644 index 000000000..d9afbd414 --- /dev/null +++ b/README-de.md @@ -0,0 +1,57 @@ + + +
+

Dokploy

+
+ +
+Reflex Logo +
+
+ + +Dokploy ist eine kostenlose und self-hostable Platform as a Service (PaaS), welche das hosten und managen von deinen Projekten und Datenbanken vereinfacht, das geschieht mithilfe von Docker und Treafik. Es ist designt, um deine Leistung und die Sicherheit deiner Projekte zu verbessern. Dokploy erlaubt dir schnell und einfach auf jeder VPS deine Projekte zu verwirklichen. + + +## Erklärung +[English](README.md) | [中文](README-zh.md) | [Deutsch](README-de.md) | [Русский Язык](README-ru.md) + + + + +## 🌟 Vorteile + +- **Projekte**: - **Projekte**: Hoste jegliche Art von Projekt (Node.js, PHP, Python, Go, Ruby, etc.) mit Einfachheit. +- **Datenbanken**: Erstelle und manage Datenbanken, wie MySQL, PostgreSQL, MongoDB, MariaDB, Redis, und mehr. +- **Docker Management**: Einfach Docker container hosten und managen. +- **Traefik Integration**: Automatische Integration mit Traefik für routing und load balancing +- **Real-time Monitoring**: Monitor von CPU, RAM, Speicher, und network Nutzung. +- **Database Backups**: Automatische Backups mit Support für mehrere Speicher Systeme. + + +## 🚀 Loslegen + +Um anzufangen führe einfach den folgende command in einer VPS aus: + +```bash +curl -sSL https://dokploy.com/install.sh | sh +``` + +Getestete Systems: + +- Ubuntu 24.04 LTS (Noble Numbat) +- Ubuntu 23.10 (Mantic Minotaur) +- Ubuntu 22.04 LTS (Jammy Jellyfish) +- Ubuntu 20.04 LTS (Focal Fossa) +- Ubuntu 18.04 LTS (Bionic Beaver) +- Debian 12 +- Debian 11 +- Fedora 40 +- Centos 9 +- Centos 8 + + +## 📄 Dokumentation + +Für eine detaillierte Dokumentation, siehe [docs.dokploy.com/docs](https://docs.dokploy.com) + diff --git a/README-ru.md b/README-ru.md new file mode 100644 index 000000000..52881a4ef --- /dev/null +++ b/README-ru.md @@ -0,0 +1,56 @@ +
+

Dokploy

+
+ +
+Логотип Dokploy +
+
+ + + +Dokploy - это бесплатная самоустанавливаемая Платформа как Сервис (PaaS), которая упрощает развертывание и управление приложениями и базами данных с использованием Docker и Traefik. Разработанный для повышения эффективности и безопасности, Dokploy позволяет развертывать ваши приложения на любом VPS. + + + +## Объяснение +[English](README.md) | [中文](README-zh.md) | [Deutsch](README-de.md) + + + + +## 🌟 Особенности + +- **Приложения**: Легко развертывать любой тип приложения (Node.js, PHP, Python, Go, Ruby и др.). +- **Базы данных**: Создавайте и управляйте базами данных с поддержкой MySQL, PostgreSQL, MongoDB, MariaDB, Redis и других. +- **Управление Docker**: Легко развертывать и управляйте контейнерами Docker. +- **Интеграция с Traefik**: Автоматически интегрируется с Traefik для маршрутизации и балансировки нагрузки. +- **Мониторинг в реальном времени**: Отслеживайте использование CPU, памяти, хранилища и сети. +- **Резервное копирование баз данных**: Автоматизируйте резервное копирование с поддержкой нескольких мест хранения. + + +## 🚀 Начало работы + +Чтобы установить, выполните следующую команду на VPS: + + +```bash +curl -sSL https://dokploy.com/install.sh | sh +``` + +Проверенные системы: + +- Ubuntu 24.04 LTS (Noble Numbat) +- Ubuntu 23.10 (Mantic Minotaur) +- Ubuntu 22.04 LTS (Jammy Jellyfish) +- Ubuntu 20.04 LTS (Focal Fossa) +- Ubuntu 18.04 LTS (Bionic Beaver) +- Debian 12 +- Debian 11 +- Fedora 40 +- Centos 9 +- Centos 8 + + +## 📄 Документация +Для подробной документации посетите [docs.dokploy.com/docs](https://docs.dokploy.com). diff --git a/README-zh.md b/README-zh.md new file mode 100644 index 000000000..35c7fc1f9 --- /dev/null +++ b/README-zh.md @@ -0,0 +1,60 @@ + + +
+

Dokploy

+
+ +
+Reflex Logo +
+
+ + + +Dokploy 是一个免费的自托管平台即服务 (PaaS),它使用 Docker 和 Traefik 简化了应用程序和数据库的部署和管理。 Dokploy 旨在提高效率和安全性,允许您在任何 VPS 上部署应用程序。 + +## 语言 +[English](README.md) + +[中文](README-zh.md) + +[Deutsch](README-de.md) + +[Русский Язык](README-ru.md) + + + + + +## 🌟 功能 + +- **应用程序**: 轻松部署任何类型的应用程序(Node.js,PHP,Python,Go、Ruby 等)。数据库: 创建和管理数据库,支持 MySQL,PostgreSQL,MongoDB、MariaDB、Redis 等。 +- **Docker 管理**: 轻松部署和管理 Docker 容器。 +- **Traefik 集成**: 自动与 Traefik 集成,用于路由和负载均衡。 +- **实时监控**: 监控 CPU,内存,存储和网络使用情况。 +- **数据库备份**: 支持多种存储目的地自动备份。 + +## 🚀 入门 +要开始使用 请在VPS 上运行以下命令: + +```bash +curl -sSL https://dokploy.com/install.sh | sh +``` + +经过测试的系统: + +- Ubuntu 24.04 LTS (Noble Numbat) +- Ubuntu 23.10 (Mantic Minotaur) +- Ubuntu 22.04 LTS (Jammy Jellyfish) +- Ubuntu 20.04 LTS (Focal Fossa) +- Ubuntu 18.04 LTS (Bionic Beaver) +- Debian 12 +- Debian 11 +- Fedora 40 +- Centos 9 +- Centos 8 + + +## 📄 文档 + +如需查看详细的文档资料 请访问[docs.dokploy.com/docs](https://docs.dokploy.com) diff --git a/README.md b/README.md index 19e44df07..58cb0961c 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,12 @@ Dokploy is a free self-hostable Platform as a Service (PaaS) that simplifies the +## Explanation +[English](README.md) | [中文](README-zh.md) | [Deutsch](README-de.md) | [Русский Язык](README-ru.md) + + + + ## 🌟 Features - **Applications**: Deploy any type of application (Node.js, PHP, Python, Go, Ruby, etc.) with ease. @@ -36,8 +42,16 @@ curl -sSL https://dokploy.com/install.sh | sh Tested Systems: -- Ubuntu 20.04 +- Ubuntu 24.04 LTS (Noble Numbat) +- Ubuntu 23.10 (Mantic Minotaur) +- Ubuntu 22.04 LTS (Jammy Jellyfish) +- Ubuntu 20.04 LTS (Focal Fossa) +- Ubuntu 18.04 LTS (Bionic Beaver) +- Debian 12 - Debian 11 +- Fedora 40 +- Centos 9 +- Centos 8 ## 📄 Documentation diff --git a/TERMS_AND_CONDITIONS.md b/TERMS_AND_CONDITIONS.md index acf376cd0..56ef97fb1 100644 --- a/TERMS_AND_CONDITIONS.md +++ b/TERMS_AND_CONDITIONS.md @@ -1,25 +1,22 @@ # Terms & Conditions +**Dokploy core** is a free and open-source solution intended as an alternative to established cloud platforms like Vercel and Netlify. -Dokploy core is a free and open-source program alternative to Vercel, Netlify, and other cloud services. +The Dokploy team endeavors to mitigate potential defects and issues through stringent testing and adherence to principles of clean coding. Dokploy is provided "AS IS" without any warranties, express or implied. Refer to the [License](https://github.com/Dokploy/Dokploy/blob/main/LICENSE) for details on permissions and restrictions. -Developers of Dokploy do their best to prevent bugs and issues through rigorous testing processes and clean code principles. Dokploy is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and limitations under the [License](https://github.com/Dokploy/Dokploy/blob/main/LICENSE). - -By using Dokploy you agree to Terms and Conditions, and the license of Dokploy. ### Description of Service: -Dokploy core is an open-source program designed to streamline application deployment processes for personal and commercial use. Users are free to install, modify, and run Dokploy on their own machines or within their organizations to enhance their development and deployment workflows. While Dokploy encourages a wide range of uses to foster innovation and efficiency, it is crucial to note that selling Dokploy itself as a service or repackaging it as part of a commercial offering without explicit permission is strictly prohibited. This ensures that the open-source nature of Dokploy remains intact and benefits the community as a whole. +**Dokploy core** is an open-source tool designed to simplify the deployment of applications for both personal and business use. Users are permitted to install, modify, and operate Dokploy independently or within their organizations to improve their development and deployment operations. It is important to note that any commercial resale or redistribution of Dokploy as a service is strictly forbidden without explicit consent. This prohibition ensures the preservation of Dokploy's open-source character for the benefit of the entire community. ### Our Responsibility -Dokploy developers will do their best to ensure that Dokploy remains functional and major bugs are resolved quickly. If you have a feature request, you are more than welcome to open a request for it, but the ultimate decision whether or not the feature will be added is taken by Dokploy's core developers. +The Dokploy development team commits to maintaining the functionality of the software and addressing major issues promptly. While we welcome suggestions for new features, the decision to include them rests solely with the core developers of Dokploy. ### Usage Data -Dokploy doesn't collect any usage data. It is a free and open-source program, and it is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +**Dokploy** does not collect any user data. It is distributed as a free and open-source tool under the terms of "AS IS", without any implied warranties or conditions. -### Future changes +### Future Changes -Terms of Service / Terms & Conditions may change at any point without a prior notice. \ No newline at end of file +The Terms of Service and Terms & Conditions are subject to change without prior notice. diff --git a/__test__/compose/compose.test.ts b/__test__/compose/compose.test.ts new file mode 100644 index 000000000..675cb7729 --- /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 000000000..88e8b3572 --- /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 000000000..df2bb417f --- /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 000000000..8f17ac865 --- /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 000000000..b4a0929cf --- /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 000000000..92df7a73d --- /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 000000000..ae9387757 --- /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 000000000..861343a26 --- /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 000000000..1a58314d2 --- /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 000000000..7922eb204 --- /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 000000000..9e3bfa80c --- /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 000000000..cf4ca1328 --- /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 000000000..6188e4a8f --- /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 000000000..b9b22fdf3 --- /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 000000000..0f071b2e8 --- /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 000000000..113576aef --- /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 000000000..90905a00e --- /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 000000000..41581844e --- /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 000000000..6b87f2ca5 --- /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 000000000..331c3bf1c --- /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 000000000..98a235884 --- /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 000000000..4127903f8 --- /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/auth/login-2fa.tsx b/components/auth/login-2fa.tsx index 17b2d483e..6bf03d0c3 100644 --- a/components/auth/login-2fa.tsx +++ b/components/auth/login-2fa.tsx @@ -61,7 +61,7 @@ export const Login2FA = ({ authId }: Props) => { id: authId, }) .then(() => { - toast.success("Signin succesfully", { + toast.success("Signin successfully", { duration: 2000, }); diff --git a/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx b/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx new file mode 100644 index 000000000..3e2730c9f --- /dev/null +++ b/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx @@ -0,0 +1,764 @@ +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { api } from "@/utils/api"; +import { AlertBlock } from "@/components/shared/alert-block"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; +import { HelpCircle, Settings } from "lucide-react"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { CodeEditor } from "@/components/shared/code-editor"; + +const HealthCheckSwarmSchema = z + .object({ + Test: z.array(z.string()).optional(), + Interval: z.number().optional(), + Timeout: z.number().optional(), + StartPeriod: z.number().optional(), + Retries: z.number().optional(), + }) + .strict(); + +const RestartPolicySwarmSchema = z + .object({ + Condition: z.string().optional(), + Delay: z.number().optional(), + MaxAttempts: z.number().optional(), + Window: z.number().optional(), + }) + .strict(); + +const PreferenceSchema = z + .object({ + Spread: z.object({ + SpreadDescriptor: z.string(), + }), + }) + .strict(); + +const PlatformSchema = z + .object({ + Architecture: z.string(), + OS: z.string(), + }) + .strict(); + +const PlacementSwarmSchema = z + .object({ + Constraints: z.array(z.string()).optional(), + Preferences: z.array(PreferenceSchema).optional(), + MaxReplicas: z.number().optional(), + Platforms: z.array(PlatformSchema).optional(), + }) + .strict(); + +const UpdateConfigSwarmSchema = z + .object({ + Parallelism: z.number(), + Delay: z.number().optional(), + FailureAction: z.string().optional(), + Monitor: z.number().optional(), + MaxFailureRatio: z.number().optional(), + Order: z.string(), + }) + .strict(); + +const ReplicatedSchema = z + .object({ + Replicas: z.number().optional(), + }) + .strict(); + +const ReplicatedJobSchema = z + .object({ + MaxConcurrent: z.number().optional(), + TotalCompletions: z.number().optional(), + }) + .strict(); + +const ServiceModeSwarmSchema = z + .object({ + Replicated: ReplicatedSchema.optional(), + Global: z.object({}).optional(), + ReplicatedJob: ReplicatedJobSchema.optional(), + GlobalJob: z.object({}).optional(), + }) + .strict(); + +const NetworkSwarmSchema = z.array( + z + .object({ + Target: z.string().optional(), + Aliases: z.array(z.string()).optional(), + DriverOpts: z.object({}).optional(), + }) + .strict(), +); + +const LabelsSwarmSchema = z.record(z.string()); + +const createStringToJSONSchema = (schema: z.ZodTypeAny) => { + return z + .string() + .transform((str, ctx) => { + if (str === null || str === "") { + return null; + } + try { + return JSON.parse(str); + } catch (e) { + ctx.addIssue({ code: "custom", message: "Invalid JSON format" }); + return z.NEVER; + } + }) + .superRefine((data, ctx) => { + if (data === null) { + return; + } + + if (Object.keys(data).length === 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Object cannot be empty", + }); + return; + } + + const parseResult = schema.safeParse(data); + if (!parseResult.success) { + for (const error of parseResult.error.issues) { + const path = error.path.join("."); + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `${path} ${error.message}`, + }); + } + } + }); +}; + +const addSwarmSettings = z.object({ + healthCheckSwarm: createStringToJSONSchema(HealthCheckSwarmSchema).nullable(), + restartPolicySwarm: createStringToJSONSchema( + RestartPolicySwarmSchema, + ).nullable(), + placementSwarm: createStringToJSONSchema(PlacementSwarmSchema).nullable(), + updateConfigSwarm: createStringToJSONSchema( + UpdateConfigSwarmSchema, + ).nullable(), + rollbackConfigSwarm: createStringToJSONSchema( + UpdateConfigSwarmSchema, + ).nullable(), + modeSwarm: createStringToJSONSchema(ServiceModeSwarmSchema).nullable(), + labelsSwarm: createStringToJSONSchema(LabelsSwarmSchema).nullable(), + networkSwarm: createStringToJSONSchema(NetworkSwarmSchema).nullable(), +}); + +type AddSwarmSettings = z.infer; + +interface Props { + applicationId: string; +} + +export const AddSwarmSettings = ({ applicationId }: Props) => { + const { data, refetch } = api.application.one.useQuery( + { + applicationId, + }, + { + enabled: !!applicationId, + }, + ); + + const { mutateAsync, isError, error, isLoading } = + api.application.update.useMutation(); + + const form = useForm({ + defaultValues: { + healthCheckSwarm: null, + restartPolicySwarm: null, + placementSwarm: null, + updateConfigSwarm: null, + rollbackConfigSwarm: null, + modeSwarm: null, + labelsSwarm: null, + networkSwarm: null, + }, + resolver: zodResolver(addSwarmSettings), + }); + + useEffect(() => { + if (data) { + form.reset({ + healthCheckSwarm: data.healthCheckSwarm + ? JSON.stringify(data.healthCheckSwarm, null, 2) + : null, + restartPolicySwarm: data.restartPolicySwarm + ? JSON.stringify(data.restartPolicySwarm, null, 2) + : null, + placementSwarm: data.placementSwarm + ? JSON.stringify(data.placementSwarm, null, 2) + : null, + updateConfigSwarm: data.updateConfigSwarm + ? JSON.stringify(data.updateConfigSwarm, null, 2) + : null, + rollbackConfigSwarm: data.rollbackConfigSwarm + ? JSON.stringify(data.rollbackConfigSwarm, null, 2) + : null, + modeSwarm: data.modeSwarm + ? JSON.stringify(data.modeSwarm, null, 2) + : null, + labelsSwarm: data.labelsSwarm + ? JSON.stringify(data.labelsSwarm, null, 2) + : null, + networkSwarm: data.networkSwarm + ? JSON.stringify(data.networkSwarm, null, 2) + : null, + }); + } + }, [form, form.reset, data]); + + const onSubmit = async (data: AddSwarmSettings) => { + await mutateAsync({ + applicationId, + healthCheckSwarm: data.healthCheckSwarm, + restartPolicySwarm: data.restartPolicySwarm, + placementSwarm: data.placementSwarm, + updateConfigSwarm: data.updateConfigSwarm, + rollbackConfigSwarm: data.rollbackConfigSwarm, + modeSwarm: data.modeSwarm, + labelsSwarm: data.labelsSwarm, + networkSwarm: data.networkSwarm, + }) + .then(async () => { + toast.success("Swarm settings updated"); + refetch(); + }) + .catch(() => { + toast.error("Error to update the swarm settings"); + }); + }; + return ( + + + + + + + Swarm Settings + + Update certain settings using a json object. + + + {isError && {error?.message}} + +
+ + ( + + Health Check + + + + + Check the interface + + + + + +
+														{`{
+	Test?: string[] | undefined;
+	Interval?: number | undefined;
+	Timeout?: number | undefined;
+	StartPeriod?: number | undefined;
+	Retries?: number | undefined;
+}`}
+													
+
+
+
+
+ + + + +
+										
+									
+
+ )} + /> + + ( + + Restart Policy + + + + + Check the interface + + + + + +
+														{`{
+	Condition?: string | undefined;
+	Delay?: number | undefined;
+	MaxAttempts?: number | undefined;
+	Window?: number | undefined;
+}`}
+													
+
+
+
+
+ + + + +
+										
+									
+
+ )} + /> + + ( + + Placement + + + + + Check the interface + + + + + +
+														{`{
+	Constraints?: string[] | undefined;
+	Preferences?: Array<{ Spread: { SpreadDescriptor: string } }> | undefined;
+	MaxReplicas?: number | undefined;
+	Platforms?:
+		| Array<{
+				Architecture: string;
+				OS: string;
+		  }>
+		| undefined;
+}`}
+													
+
+
+
+
+ + + + +
+										
+									
+
+ )} + /> + + ( + + Update Config + + + + + Check the interface + + + + + +
+														{`{
+	Parallelism?: number;
+	Delay?: number | undefined;
+	FailureAction?: string | undefined;
+	Monitor?: number | undefined;
+	MaxFailureRatio?: number | undefined;
+	Order: string;
+}`}
+													
+
+
+
+
+ + + + +
+										
+									
+
+ )} + /> + + ( + + Rollback Config + + + + + Check the interface + + + + + +
+														{`{
+	Parallelism?: number;
+	Delay?: number | undefined;
+	FailureAction?: string | undefined;
+	Monitor?: number | undefined;
+	MaxFailureRatio?: number | undefined;
+	Order: string;
+}`}
+													
+
+
+
+
+ + + + +
+										
+									
+
+ )} + /> + + ( + + Mode + + + + + Check the interface + + + + + +
+														{`{
+	Replicated?: { Replicas?: number | undefined } | undefined;
+	Global?: {} | undefined;
+	ReplicatedJob?:
+		| {
+				MaxConcurrent?: number | undefined;
+				TotalCompletions?: number | undefined;
+		  }
+		| undefined;
+	GlobalJob?: {} | undefined;
+}`}
+													
+
+
+
+
+ + + + +
+										
+									
+
+ )} + /> + ( + + Network + + + + + Check the interface + + + + + +
+														{`[
+  {
+	"Target" : string | undefined;
+	"Aliases" : string[] | undefined;
+	"DriverOpts" : { [key: string]: string } | undefined;
+  }
+]`}
+													
+
+
+
+
+ + + +
+										
+									
+
+ )} + /> + ( + + Labels + + + + + Check the interface + + + + + +
+														{`{
+	[name: string]: string;
+}`}
+													
+
+
+
+
+ + + +
+										
+									
+
+ )} + /> + + + + + + +
+
+ ); +}; diff --git a/components/dashboard/application/advanced/cluster/show-cluster-settings.tsx b/components/dashboard/application/advanced/cluster/show-cluster-settings.tsx new file mode 100644 index 000000000..da69aedb8 --- /dev/null +++ b/components/dashboard/application/advanced/cluster/show-cluster-settings.tsx @@ -0,0 +1,208 @@ +import React from "react"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { api } from "@/utils/api"; +import { z } from "zod"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { toast } from "sonner"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import Link from "next/link"; +import { Server } from "lucide-react"; +import { AddSwarmSettings } from "./modify-swarm-settings"; +import { AlertBlock } from "@/components/shared/alert-block"; + +interface Props { + applicationId: string; +} + +const AddRedirectchema = z.object({ + replicas: z.number(), + registryId: z.string(), +}); + +type AddCommand = z.infer; + +export const ShowClusterSettings = ({ applicationId }: Props) => { + const { data } = api.application.one.useQuery( + { + applicationId, + }, + { enabled: !!applicationId }, + ); + + const { data: registries } = api.registry.all.useQuery(); + + const utils = api.useUtils(); + + const { mutateAsync, isLoading } = api.application.update.useMutation(); + + const form = useForm({ + defaultValues: { + registryId: data?.registryId || "", + replicas: data?.replicas || 1, + }, + resolver: zodResolver(AddRedirectchema), + }); + + useEffect(() => { + if (data?.command) { + form.reset({ + registryId: data?.registryId || "", + replicas: data?.replicas || 1, + }); + } + }, [form, form.reset, form.formState.isSubmitSuccessful, data?.command]); + + const onSubmit = async (data: AddCommand) => { + await mutateAsync({ + applicationId, + registryId: data?.registryId === "none" ? null : data?.registryId, + replicas: data?.replicas, + }) + .then(async () => { + toast.success("Command Updated"); + await utils.application.one.invalidate({ + applicationId, + }); + }) + .catch(() => { + toast.error("Error to update the command"); + }); + }; + + return ( + + +
+ Cluster Settings + + Add the registry and the replicas of the application + +
+ +
+ + + Please remember to click Redeploy after modify the cluster settings to + apply the changes. + +
+ +
+ ( + + Replicas + + { + field.onChange(Number(e.target.value)); + }} + type="number" + /> + + + + + )} + /> +
+ + {registries && registries?.length === 0 ? ( +
+
+ + + To use a cluster feature, you need to configure at least a + registry first. Please, go to{" "} + + Settings + {" "} + to do so. + +
+
+ ) : ( + <> + ( + + Select a registry + + + )} + /> + + )} + +
+ +
+ + +
+
+ ); +}; diff --git a/components/dashboard/application/advanced/general/add-command.tsx b/components/dashboard/application/advanced/general/add-command.tsx index b7de2ae62..a898607d7 100644 --- a/components/dashboard/application/advanced/general/add-command.tsx +++ b/components/dashboard/application/advanced/general/add-command.tsx @@ -26,11 +26,11 @@ interface Props { applicationId: string; } -const AddRedirectchema = z.object({ +const AddRedirectSchema = z.object({ command: z.string(), }); -type AddCommand = z.infer; +type AddCommand = z.infer; export const AddCommand = ({ applicationId }: Props) => { const { data } = api.application.one.useQuery( @@ -48,7 +48,7 @@ export const AddCommand = ({ applicationId }: Props) => { defaultValues: { command: "", }, - resolver: zodResolver(AddRedirectchema), + resolver: zodResolver(AddRedirectSchema), }); useEffect(() => { diff --git a/components/dashboard/application/advanced/ports/add-port.tsx b/components/dashboard/application/advanced/ports/add-port.tsx index ceb9657cb..76939d821 100644 --- a/components/dashboard/application/advanced/ports/add-port.tsx +++ b/components/dashboard/application/advanced/ports/add-port.tsx @@ -18,8 +18,8 @@ import { } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { api } from "@/utils/api"; +import { AlertBlock } from "@/components/shared/alert-block"; import { zodResolver } from "@hookform/resolvers/zod"; -import { AlertTriangle } from "lucide-react"; import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; @@ -33,7 +33,7 @@ import { } from "@/components/ui/select"; import { z } from "zod"; -const AddPortchema = z.object({ +const AddPortSchema = z.object({ publishedPort: z.number().int().min(1).max(65535), targetPort: z.number().int().min(1).max(65535), protocol: z.enum(["tcp", "udp"], { @@ -41,7 +41,7 @@ const AddPortchema = z.object({ }), }); -type AddPort = z.infer; +type AddPort = z.infer; interface Props { applicationId: string; @@ -62,7 +62,7 @@ export const AddPort = ({ publishedPort: 0, targetPort: 0, }, - resolver: zodResolver(AddPortchema), + resolver: zodResolver(AddPortSchema), }); useEffect(() => { @@ -100,14 +100,7 @@ export const AddPort = ({ Ports are used to expose your application to the internet. - {isError && ( -
- - - {error?.message} - -
- )} + {isError && {error?.message}}
- - None + TCP UDP diff --git a/components/dashboard/application/advanced/ports/show-port.tsx b/components/dashboard/application/advanced/ports/show-port.tsx index 6ecd4b41e..ab5f40970 100644 --- a/components/dashboard/application/advanced/ports/show-port.tsx +++ b/components/dashboard/application/advanced/ports/show-port.tsx @@ -11,6 +11,7 @@ import { Rss } from "lucide-react"; import { AddPort } from "./add-port"; import { DeletePort } from "./delete-port"; import { UpdatePort } from "./update-port"; +import { AlertBlock } from "@/components/shared/alert-block"; interface Props { applicationId: string; } @@ -47,7 +48,11 @@ export const ShowPorts = ({ applicationId }: Props) => { Add Port ) : ( -
+
+ + Please remember to click Redeploy after adding, editing, or + deleting the ports to apply the changes. +
{data?.ports.map((port) => (
diff --git a/components/dashboard/application/advanced/ports/update-port.tsx b/components/dashboard/application/advanced/ports/update-port.tsx index 8bee5a816..9742964e2 100644 --- a/components/dashboard/application/advanced/ports/update-port.tsx +++ b/components/dashboard/application/advanced/ports/update-port.tsx @@ -18,8 +18,9 @@ import { } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { api } from "@/utils/api"; +import { AlertBlock } from "@/components/shared/alert-block"; import { zodResolver } from "@hookform/resolvers/zod"; -import { AlertTriangle, Pencil } from "lucide-react"; +import { PenBoxIcon, Pencil } from "lucide-react"; import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; @@ -98,7 +99,7 @@ export const UpdatePort = ({ portId }: Props) => { @@ -106,14 +107,7 @@ export const UpdatePort = ({ portId }: Props) => { Update Update the port - {isError && ( -
- - - {error?.message} - -
- )} + {isError && {error?.message}} - {isError && ( -
- - - {error?.message} - -
- )} + {isError && {error?.message}} { @@ -101,14 +102,7 @@ export const UpdateRedirect = ({ redirectId }: Props) => { Update Update the redirect - {isError && ( -
- - - {error?.message} - -
- )} + {isError && {error?.message}} - {isError && ( -
- - - {error?.message} - -
- )} + {isError && {error?.message}} { @@ -96,14 +97,7 @@ export const UpdateSecurity = ({ securityId }: Props) => { Update Update the security - {isError && ( -
- - - {error?.message} - -
- )} + {isError && {error?.message}} { + + Please remember to click Redeploy after modify the resources to apply + the changes. + {
) : (
-
-
-
{data || "Empty"}
-
-
+
+ +
diff --git a/components/dashboard/application/advanced/traefik/update-traefik-config.tsx b/components/dashboard/application/advanced/traefik/update-traefik-config.tsx index f4422cd86..13bec2424 100644 --- a/components/dashboard/application/advanced/traefik/update-traefik-config.tsx +++ b/components/dashboard/application/advanced/traefik/update-traefik-config.tsx @@ -16,15 +16,15 @@ import { FormLabel, FormMessage, } from "@/components/ui/form"; -import { Textarea } from "@/components/ui/textarea"; import { api } from "@/utils/api"; +import { AlertBlock } from "@/components/shared/alert-block"; import { zodResolver } from "@hookform/resolvers/zod"; -import { AlertTriangle } from "lucide-react"; import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; import jsyaml from "js-yaml"; +import { CodeEditor } from "@/components/shared/code-editor"; const UpdateTraefikConfigSchema = z.object({ traefikConfig: z.string(), @@ -116,20 +116,13 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => { Update traefik config Update the traefik config - {isError && ( -
- - - {error?.message} - -
- )} + {isError && {error?.message}}
{ Traefik config -